Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91e5f6b27c | |||
| 571c985251 | |||
| 4405744f56 | |||
| 8d902a269f |
@@ -1,49 +0,0 @@
|
||||
name: Deploy DNS entries
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ADGUARD_USER: ${{ vars.ADGUARD_USER }}
|
||||
ADGUARD_PASSWORD: ${{ secrets.ADGUARD_PASSWORD }}
|
||||
ADGUARD_URL: ${{ vars.ADGUARD_URL }}
|
||||
JSON_FILE: ${{ vars.JSON_FILE }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Python dependencies in virtualenv
|
||||
run: |
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Deploy DNS entries to AdGuard Home
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python main.py --sync
|
||||
shell: bash
|
||||
env:
|
||||
ADGUARD_USER: ${{ env.ADGUARD_USER }}
|
||||
ADGUARD_PASSWORD: ${{ env.ADGUARD_PASSWORD }}
|
||||
ADGUARD_URL: ${{ env.ADGUARD_URL }}
|
||||
JSON_FILE: ${{ env.JSON_FILE }}
|
||||
continue-on-error: false
|
||||
timeout-minutes: 10
|
||||
id: deploy_dns
|
||||
|
||||
- name: Deployment Result
|
||||
if: success()
|
||||
run: echo "✅ DNS entries deployed successfully to AdGuard Home."
|
||||
73
.gitea/workflows/ci.yaml.bak
Normal file
73
.gitea/workflows/ci.yaml.bak
Normal file
@@ -0,0 +1,73 @@
|
||||
# .gitea/workflows/ci.yml
|
||||
name: CI/CD Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # Trigger auf den Main-Branch (kannst du anpassen)
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest # Du kannst auch einen anderen selbstgehosteten Runner verwenden
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
echo "Running tests..."
|
||||
echo "Checking for python3..."
|
||||
if command -v python3 &> /dev/null; then
|
||||
echo "Python found"
|
||||
python3 -c "print('Python is working')"
|
||||
else
|
||||
echo "Python not found"
|
||||
fi
|
||||
|
||||
# - name: Export config vars as environment
|
||||
# env:
|
||||
# ADGUARD_URL: ${{ vars.ADGUARD_URL }}
|
||||
# ADGUARD_USER: ${{ vars.ADGUARD_USER }}
|
||||
# ADGUARD_PASSWORD: ${{ secrets.ADGUARD_PASSWORD }}
|
||||
# YAML_FILE: ${{ vars.YAML_FILE }}
|
||||
# run: |
|
||||
# echo "Exporting config vars..."
|
||||
# echo "URL: $ADGUARD_URL"
|
||||
# echo "User: $ADGUARD_USER"
|
||||
# echo "YAML_FILE: $YAML_FILE"
|
||||
|
||||
# deploy:
|
||||
# name: Deploy
|
||||
# runs-on: ubuntu-latest # Du kannst auch einen selbstgehosteten Runner verwenden
|
||||
# needs: test # Wartet auf das Test-Job
|
||||
# steps:
|
||||
# - name: Checkout Code
|
||||
# uses: actions/checkout@v3
|
||||
|
||||
|
||||
# - name: Install required Python packages
|
||||
# run: |
|
||||
# echo "Installing required Python packages..."
|
||||
# pip install requests pyyaml python-dotenv
|
||||
|
||||
# - name: Export config vars as environment
|
||||
# env:
|
||||
# ADGUARD_URL: ${{ vars.ADGUARD_URL }}
|
||||
# ADGUARD_USER: ${{ vars.ADGUARD_USER }}
|
||||
# ADGUARD_PASSWORD: ${{ secrets.ADGUARD_PASSWORD }}
|
||||
# YAML_FILE: ${{ vars.YAML_FILE }}
|
||||
# run: |
|
||||
# echo "Exporting config vars..."
|
||||
# echo "URL: $ADGUARD_URL"
|
||||
# echo "User: $ADGUARD_USER"
|
||||
# echo "YAML_FILE: $YAML_FILE"
|
||||
|
||||
# # - name: Run the Python script
|
||||
# # run: |
|
||||
# # echo "Deploying application..."
|
||||
# # python3 main.py # Dein Python-Skript ausführen
|
||||
|
||||
environment:
|
||||
name: production
|
||||
if: branch == 'main' # Läuft nur für den `main`-Branch
|
||||
22
.gitea/workflows/test.yaml
Normal file
22
.gitea/workflows/test.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Gitea Actions Demo
|
||||
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
|
||||
Run-Python-Script:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
# Load environment variables from gitea secrets and variables
|
||||
- run: pip install -r requirements.txt
|
||||
- run: python main.py
|
||||
env:
|
||||
ADGUARD_URL: ${{ vars.ADGUARD_URL }}
|
||||
ADGUARD_USER: ${{ vars.ADGUARD_USER }}
|
||||
ADGUARD_PASSWORD: ${{ secrets.ADGUARD_PASSWORD }}
|
||||
YAML_FILE: ${{ vars.YAML_FILE }}
|
||||
- run: echo "Python script executed successfully!"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -168,4 +168,3 @@ cython_debug/
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
.claude/
|
||||
|
||||
103
client.go
Normal file
103
client.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type AdGuardClient struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
func NewAdGuardClient() (*AdGuardClient, error) {
|
||||
baseURL := os.Getenv("ADGUARD_URL")
|
||||
user := os.Getenv("ADGUARD_USER")
|
||||
pass := os.Getenv("ADGUARD_PASSWORD")
|
||||
|
||||
if baseURL == "" || user == "" || pass == "" {
|
||||
return nil, fmt.Errorf("fehlende Umgebungsvariablen")
|
||||
}
|
||||
|
||||
return &AdGuardClient{
|
||||
BaseURL: baseURL,
|
||||
Username: user,
|
||||
Password: pass,
|
||||
Client: &http.Client{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *AdGuardClient) Login() error {
|
||||
data := map[string]string{"name": c.Username, "password": c.Password}
|
||||
return c.post("/control/login", data, nil)
|
||||
}
|
||||
|
||||
func (c *AdGuardClient) GetRewrites() ([]Rewrite, error) {
|
||||
resp, err := c.Client.Get(c.BaseURL + "/control/rewrite/list")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var list []Rewrite
|
||||
if err := json.NewDecoder(resp.Body).Decode(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (c *AdGuardClient) AddRewrite(r Rewrite) error {
|
||||
return c.post("/control/rewrite/add", r, nil)
|
||||
}
|
||||
|
||||
func (c *AdGuardClient) DeleteRewrite(r Rewrite) error {
|
||||
return c.post("/control/rewrite/delete", r, nil)
|
||||
}
|
||||
|
||||
func (c *AdGuardClient) UpdateRewrite(oldR, newR Rewrite) error {
|
||||
payload := map[string]Rewrite{"target": oldR, "update": newR}
|
||||
return c.put("/control/rewrite/update", payload, nil)
|
||||
}
|
||||
|
||||
func (c *AdGuardClient) post(path string, body any, result any) error {
|
||||
return c.doRequest("POST", path, body, result)
|
||||
}
|
||||
|
||||
func (c *AdGuardClient) put(path string, body any, result any) error {
|
||||
return c.doRequest("PUT", path, body, result)
|
||||
}
|
||||
|
||||
func (c *AdGuardClient) doRequest(method, path string, body any, result any) error {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, c.BaseURL+path, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("Fehler %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
42
main.go
Normal file
42
main.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
LoadEnv()
|
||||
|
||||
client, err := NewAdGuardClient()
|
||||
if err != nil {
|
||||
log.Fatalf("Fehler beim Initialisieren des Clients: %v", err)
|
||||
}
|
||||
|
||||
if err := client.Login(); err != nil {
|
||||
log.Fatalf("Login fehlgeschlagen: %v", err)
|
||||
}
|
||||
log.Println("Login erfolgreich.")
|
||||
|
||||
current, err := client.GetRewrites()
|
||||
if err != nil {
|
||||
log.Fatalf("Fehler beim Abrufen der Rewrites: %v", err)
|
||||
}
|
||||
|
||||
desired, err := LoadYAML()
|
||||
if err != nil {
|
||||
log.Fatalf("Fehler beim Laden der YAML-Datei: %v", err)
|
||||
}
|
||||
|
||||
merged := MergeRewrites(current, desired)
|
||||
|
||||
if err := SaveYAML(merged); err != nil {
|
||||
log.Fatalf("Fehler beim Speichern der YAML-Datei: %v", err)
|
||||
}
|
||||
|
||||
if err := client.ApplyRewrites(merged); err != nil {
|
||||
log.Fatalf("Fehler beim Anwenden der Änderungen: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Alle Änderungen wurden erfolgreich angewendet.")
|
||||
}
|
||||
425
main.py
425
main.py
@@ -1,382 +1,131 @@
|
||||
import requests
|
||||
import json
|
||||
import yaml
|
||||
import os
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
# from dotenv import load_dotenv
|
||||
|
||||
# Check if environment variables are already set (e.g., in CI/CD)
|
||||
# If not, load them from .env file
|
||||
required_vars = ["ADGUARD_URL", "ADGUARD_USER", "ADGUARD_PASSWORD", "JSON_FILE"]
|
||||
env_vars_already_set = all(os.getenv(var) for var in required_vars)
|
||||
# load_dotenv()
|
||||
|
||||
if env_vars_already_set:
|
||||
print("Using environment variables from system/CI/CD")
|
||||
else:
|
||||
# Try to load from .env file
|
||||
if os.path.exists(".env"):
|
||||
print("Loading environment variables from .env file")
|
||||
load_dotenv()
|
||||
else:
|
||||
print("No .env file found, expecting environment variables to be set")
|
||||
url = os.getenv("ADGUARD_URL")
|
||||
username = os.getenv("ADGUARD_USER")
|
||||
password = os.getenv("ADGUARD_PASSWORD")
|
||||
yaml_file = os.getenv("YAML_FILE")
|
||||
|
||||
print(f"AdGuard URL: {url}")
|
||||
print(f"Username: {username}")
|
||||
|
||||
# Load environment variables (validation happens later)
|
||||
url = None
|
||||
username = None
|
||||
password = None
|
||||
json_file = None
|
||||
session = requests.Session()
|
||||
|
||||
def validate_environment():
|
||||
"""Validate required environment variables."""
|
||||
global url, username, password, json_file
|
||||
|
||||
url = os.getenv("ADGUARD_URL")
|
||||
username = os.getenv("ADGUARD_USER")
|
||||
password = os.getenv("ADGUARD_PASSWORD")
|
||||
json_file = os.getenv("JSON_FILE")
|
||||
|
||||
errors = []
|
||||
if not url:
|
||||
errors.append("ADGUARD_URL environment variable is not set")
|
||||
if not username:
|
||||
errors.append("ADGUARD_USER environment variable is not set")
|
||||
if not password:
|
||||
errors.append("ADGUARD_PASSWORD environment variable is not set")
|
||||
if not json_file:
|
||||
errors.append("JSON_FILE environment variable is not set")
|
||||
|
||||
if errors:
|
||||
print("ERROR: Missing required environment variables:")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
print("\nPlease set these in your .env file or environment.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"AdGuard URL: {url}")
|
||||
print(f"Username: {username}")
|
||||
print(f"JSON File: {json_file}")
|
||||
|
||||
def validate_json_structure(data):
|
||||
"""Validate the JSON structure and content."""
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("JSON root must be an object/dictionary")
|
||||
|
||||
if "rewrites" not in data:
|
||||
raise ValueError("JSON must contain a 'rewrites' key")
|
||||
|
||||
rewrites = data["rewrites"]
|
||||
if not isinstance(rewrites, list):
|
||||
raise ValueError("'rewrites' must be an array/list")
|
||||
|
||||
if len(rewrites) == 0:
|
||||
print("WARNING: 'rewrites' array is empty")
|
||||
return True
|
||||
|
||||
for idx, entry in enumerate(rewrites):
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError(f"Entry at index {idx} is not an object/dictionary")
|
||||
|
||||
if "domain" not in entry:
|
||||
raise ValueError(f"Entry at index {idx} is missing 'domain' field")
|
||||
|
||||
if "answer" not in entry:
|
||||
raise ValueError(f"Entry at index {idx} is missing 'answer' field")
|
||||
|
||||
if not isinstance(entry["domain"], str) or not entry["domain"].strip():
|
||||
raise ValueError(f"Entry at index {idx} has invalid 'domain' field (must be non-empty string)")
|
||||
|
||||
if not isinstance(entry["answer"], str) or not entry["answer"].strip():
|
||||
raise ValueError(f"Entry at index {idx} has invalid 'answer' field (must be non-empty string)")
|
||||
|
||||
print(f"JSON validation successful: {len(rewrites)} entries found")
|
||||
return True
|
||||
class IndentDumper(yaml.Dumper):
|
||||
def increase_indent(self, flow=False, indentless=False):
|
||||
return super().increase_indent(flow, False)
|
||||
|
||||
def login():
|
||||
"""Login to AdGuard API."""
|
||||
try:
|
||||
r = session.post(f"{url}/control/login", json={"name": username, "password": password}, timeout=10)
|
||||
if r.status_code != 200:
|
||||
print(f"ERROR: Login failed with status {r.status_code}")
|
||||
print(f"Response: {r.text}")
|
||||
sys.exit(1)
|
||||
print("Login erfolgreich.")
|
||||
except requests.exceptions.Timeout:
|
||||
print("ERROR: Login request timed out")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"ERROR: Could not connect to AdGuard at {url}")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"ERROR: Login request failed: {e}")
|
||||
sys.exit(1)
|
||||
r = session.post(f"{url}/control/login", json={"name": username, "password": password})
|
||||
if r.status_code != 200:
|
||||
raise Exception(f"Login fehlgeschlagen: {r.status_code} {r.text}")
|
||||
print("Login erfolgreich.")
|
||||
|
||||
def load_json():
|
||||
"""Load and validate JSON file."""
|
||||
if not os.path.exists(json_file):
|
||||
print(f"ERROR: JSON file not found: {json_file}")
|
||||
print("Please create the file or run with --pull to fetch from AdGuard")
|
||||
sys.exit(1)
|
||||
def load_yaml():
|
||||
with open(yaml_file, "r") as f:
|
||||
return yaml.safe_load(f)["rewrites"]
|
||||
|
||||
try:
|
||||
with open(json_file, "r") as f:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"ERROR: Invalid JSON in file {json_file}")
|
||||
print(f"JSON Error: {e}")
|
||||
sys.exit(1)
|
||||
except PermissionError:
|
||||
print(f"ERROR: Permission denied reading file: {json_file}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"ERROR: Could not read file {json_file}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate JSON structure
|
||||
try:
|
||||
validate_json_structure(data)
|
||||
except ValueError as e:
|
||||
print(f"ERROR: JSON validation failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
return data["rewrites"]
|
||||
|
||||
def save_json(data):
|
||||
"""Save rewrites to JSON file."""
|
||||
try:
|
||||
# Create backup if file exists
|
||||
if os.path.exists(json_file):
|
||||
backup_file = f"{json_file}.backup"
|
||||
try:
|
||||
with open(json_file, "r") as src, open(backup_file, "w") as dst:
|
||||
dst.write(src.read())
|
||||
print(f"Created backup: {backup_file}")
|
||||
except Exception as e:
|
||||
print(f"WARNING: Could not create backup: {e}")
|
||||
|
||||
with open(json_file, "w") as f:
|
||||
json.dump(
|
||||
{"rewrites": data},
|
||||
f,
|
||||
indent=2
|
||||
)
|
||||
except PermissionError:
|
||||
print(f"ERROR: Permission denied writing to file: {json_file}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"ERROR: Could not write to file {json_file}: {e}")
|
||||
sys.exit(1)
|
||||
def save_yaml(data):
|
||||
with open(yaml_file, "w") as f:
|
||||
yaml.dump(
|
||||
{"rewrites": data},
|
||||
f,
|
||||
Dumper=IndentDumper,
|
||||
default_flow_style=False,
|
||||
sort_keys=False
|
||||
)
|
||||
|
||||
def get_rewrites():
|
||||
"""Get current rewrites from AdGuard."""
|
||||
try:
|
||||
r = session.get(f"{url}/control/rewrite/list", timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if not isinstance(data, list):
|
||||
print("ERROR: AdGuard API returned unexpected data format")
|
||||
sys.exit(1)
|
||||
return data
|
||||
except requests.exceptions.Timeout:
|
||||
print("ERROR: Request to get rewrites timed out")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"ERROR: Could not connect to AdGuard at {url}")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"ERROR: HTTP error while getting rewrites: {e}")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"ERROR: Request failed while getting rewrites: {e}")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError:
|
||||
print("ERROR: Invalid JSON response from AdGuard")
|
||||
sys.exit(1)
|
||||
r = session.get(f"{url}/control/rewrite/list")
|
||||
r.raise_for_status()
|
||||
print(r.json())
|
||||
return r.json() # AdGuard gibt direkt eine Liste zurück
|
||||
|
||||
def add_rewrite(entry):
|
||||
"""Add a rewrite entry to AdGuard."""
|
||||
try:
|
||||
r = session.post(f"{url}/control/rewrite/add", json=entry, timeout=10)
|
||||
r.raise_for_status()
|
||||
print(f"Hinzugefügt: {entry}")
|
||||
return True
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"ERROR: Timeout while adding rewrite: {entry}")
|
||||
return False
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"ERROR: Failed to add rewrite {entry}: {e}")
|
||||
print(f"Response: {e.response.text if e.response else 'No response'}")
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"ERROR: Request failed while adding rewrite {entry}: {e}")
|
||||
return False
|
||||
r = session.post(f"{url}/control/rewrite/add", json=entry)
|
||||
r.raise_for_status()
|
||||
print(f"Hinzugefügt: {entry}")
|
||||
|
||||
def delete_rewrite(entry):
|
||||
"""Delete a rewrite entry from AdGuard."""
|
||||
try:
|
||||
r = session.post(f"{url}/control/rewrite/delete", json=entry, timeout=10)
|
||||
r.raise_for_status()
|
||||
print(f"Gelöscht: {entry}")
|
||||
return True
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"ERROR: Timeout while deleting rewrite: {entry}")
|
||||
return False
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"ERROR: Failed to delete rewrite {entry}: {e}")
|
||||
print(f"Response: {e.response.text if e.response else 'No response'}")
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"ERROR: Request failed while deleting rewrite {entry}: {e}")
|
||||
return False
|
||||
r = session.post(f"{url}/control/rewrite/delete", json=entry)
|
||||
r.raise_for_status()
|
||||
print(f"Gelöscht: {entry}")
|
||||
|
||||
def update_rewrite(old_entry, new_entry):
|
||||
"""Update a rewrite entry in AdGuard."""
|
||||
try:
|
||||
r = session.put(f"{url}/control/rewrite/update", json={"target": old_entry, "update": new_entry}, timeout=10)
|
||||
r.raise_for_status()
|
||||
print(f"Aktualisiert: {old_entry} -> {new_entry}")
|
||||
return True
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"ERROR: Timeout while updating rewrite: {old_entry}")
|
||||
return False
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"ERROR: Failed to update rewrite {old_entry}: {e}")
|
||||
print(f"Response: {e.response.text if e.response else 'No response'}")
|
||||
return False
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"ERROR: Request failed while updating rewrite {old_entry}: {e}")
|
||||
return False
|
||||
r = session.put(f"{url}/control/rewrite/update", json={"target": old_entry, "update": new_entry})
|
||||
r.raise_for_status()
|
||||
print(f"Aktualisiert: {old_entry} -> {new_entry}")
|
||||
|
||||
def sync_to_adguard(desired):
|
||||
"""Sync desired rewrites to AdGuard."""
|
||||
print("Fetching current rewrites from AdGuard...")
|
||||
def merge_rewrites(existing, desired):
|
||||
# Mapping: domain -> IP (bestehend)
|
||||
existing_map = {r["domain"]: r["answer"] for r in existing}
|
||||
desired_map = {entry["domain"]: entry["answer"] for entry in desired}
|
||||
|
||||
updated = existing.copy()
|
||||
|
||||
# Hinzufügen und Ändern
|
||||
for domain, ip in desired_map.items():
|
||||
if domain in existing_map:
|
||||
# Ändern, wenn die IP-Adresse unterschiedlich ist
|
||||
if existing_map[domain] != ip:
|
||||
print(f"Ändere {domain}: {existing_map[domain]} -> {ip}")
|
||||
for r in updated:
|
||||
if r["domain"] == domain:
|
||||
r["answer"] = ip
|
||||
else:
|
||||
# Hinzufügen, wenn der Eintrag noch nicht existiert
|
||||
print(f"Füge hinzu: {domain} -> {ip}")
|
||||
updated.append({"domain": domain, "answer": ip})
|
||||
|
||||
# Entfernen von Einträgen, die nicht mehr gewünscht sind
|
||||
for domain in existing_map.keys():
|
||||
if domain not in desired_map:
|
||||
updated = [r for r in updated if r["domain"] != domain]
|
||||
print(f"Lösche: {domain} : {ip}")
|
||||
|
||||
return updated
|
||||
|
||||
def apply_rewrites(desired):
|
||||
existing = get_rewrites()
|
||||
|
||||
existing_map = {r["domain"]: r["answer"] for r in existing}
|
||||
desired_map = {r["domain"]: r["answer"] for r in desired}
|
||||
|
||||
failed_operations = []
|
||||
|
||||
# Änderungen oder Neueinträge
|
||||
updates_count = 0
|
||||
additions_count = 0
|
||||
for domain, ip in desired_map.items():
|
||||
if domain in existing_map:
|
||||
if existing_map[domain] != ip:
|
||||
if not update_rewrite(
|
||||
update_rewrite(
|
||||
{"domain": domain, "answer": existing_map[domain]},
|
||||
{"domain": domain, "answer": ip}
|
||||
):
|
||||
failed_operations.append(f"Update {domain}")
|
||||
else:
|
||||
updates_count += 1
|
||||
)
|
||||
else:
|
||||
if not add_rewrite({"domain": domain, "answer": ip}):
|
||||
failed_operations.append(f"Add {domain}")
|
||||
else:
|
||||
additions_count += 1
|
||||
add_rewrite({"domain": domain, "answer": ip})
|
||||
|
||||
# Entferne veraltete
|
||||
deletions_count = 0
|
||||
for domain, ip in existing_map.items():
|
||||
if domain not in desired_map:
|
||||
if not delete_rewrite({"domain": domain, "answer": ip}):
|
||||
failed_operations.append(f"Delete {domain}")
|
||||
else:
|
||||
deletions_count += 1
|
||||
|
||||
# Summary
|
||||
print(f"\nSync Summary:")
|
||||
print(f" Added: {additions_count}")
|
||||
print(f" Updated: {updates_count}")
|
||||
print(f" Deleted: {deletions_count}")
|
||||
|
||||
if failed_operations:
|
||||
print(f"\nWARNING: {len(failed_operations)} operation(s) failed:")
|
||||
for op in failed_operations:
|
||||
print(f" - {op}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def pull_from_adguard():
|
||||
"""Pull current rewrites from AdGuard and save to JSON file."""
|
||||
print("Pulling current rewrites from AdGuard...")
|
||||
current = get_rewrites()
|
||||
save_json(current)
|
||||
print(f"Successfully saved {len(current)} rewrites to {json_file}")
|
||||
delete_rewrite({"domain": domain, "answer": ip})
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="AdGuard DNS Rewrite Management Tool",
|
||||
epilog="The rewrites.json file is the source of truth for --sync operations."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sync",
|
||||
action="store_true",
|
||||
help="Sync rewrites from JSON file to AdGuard (add, update, delete to match JSON)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pull",
|
||||
action="store_true",
|
||||
help="Pull current rewrites from AdGuard and update the local JSON file"
|
||||
)
|
||||
login()
|
||||
current = get_rewrites()
|
||||
desired = load_yaml()
|
||||
|
||||
try:
|
||||
args = parser.parse_args()
|
||||
except SystemExit:
|
||||
raise
|
||||
# Merge existing and desired rewrites
|
||||
merged = merge_rewrites(current, desired)
|
||||
|
||||
# If no arguments provided, show error and help
|
||||
if not args.sync and not args.pull:
|
||||
parser.error("No action specified. Use --sync or --pull.")
|
||||
# Save merged rewrites back to YAML
|
||||
save_yaml(merged)
|
||||
|
||||
# Both options cannot be used together
|
||||
if args.sync and args.pull:
|
||||
parser.error("Cannot use --sync and --pull together. Choose one.")
|
||||
|
||||
# Validate environment variables after parsing args
|
||||
validate_environment()
|
||||
|
||||
try:
|
||||
# Login to AdGuard
|
||||
print("\n=== Logging in to AdGuard ===")
|
||||
login()
|
||||
|
||||
if args.sync:
|
||||
# Sync from JSON to AdGuard (JSON is source of truth)
|
||||
print("\n=== Starting sync from JSON to AdGuard ===")
|
||||
desired = load_json()
|
||||
|
||||
if len(desired) == 0:
|
||||
print("WARNING: No rewrites found in JSON file. Nothing to sync.")
|
||||
sys.exit(0)
|
||||
|
||||
success = sync_to_adguard(desired)
|
||||
|
||||
if success:
|
||||
print("\n✓ Sync completed successfully.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\n✗ Sync completed with errors.")
|
||||
sys.exit(1)
|
||||
|
||||
elif args.pull:
|
||||
# Pull from AdGuard to JSON (overwrite local JSON)
|
||||
print("\n=== Pulling from AdGuard to JSON ===")
|
||||
pull_from_adguard()
|
||||
print("\n✓ Pull completed successfully.")
|
||||
sys.exit(0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nOperation cancelled by user.")
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
print(f"\nUnexpected error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
# Apply the merged rewrites to AdGuard
|
||||
apply_rewrites(merged)
|
||||
print("Alle Änderungen wurden erfolgreich angewendet.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
86
rewrites.go
Normal file
86
rewrites.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Rewrite struct {
|
||||
Domain string `yaml:"domain" json:"domain"`
|
||||
Answer string `yaml:"answer" json:"answer"`
|
||||
}
|
||||
|
||||
func MergeRewrites(existing, desired []Rewrite) []Rewrite {
|
||||
existingMap := make(map[string]string)
|
||||
for _, r := range existing {
|
||||
existingMap[r.Domain] = r.Answer
|
||||
}
|
||||
|
||||
desiredMap := make(map[string]string)
|
||||
for _, r := range desired {
|
||||
desiredMap[r.Domain] = r.Answer
|
||||
}
|
||||
|
||||
updated := make([]Rewrite, 0)
|
||||
|
||||
// Add or update
|
||||
for domain, ip := range desiredMap {
|
||||
if existingIP, found := existingMap[domain]; found {
|
||||
if existingIP != ip {
|
||||
fmt.Printf("Ändere %s: %s -> %s\n", domain, existingIP, ip)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Füge hinzu: %s -> %s\n", domain, ip)
|
||||
}
|
||||
updated = append(updated, Rewrite{Domain: domain, Answer: ip})
|
||||
}
|
||||
|
||||
// Delete outdated
|
||||
for domain, ip := range existingMap {
|
||||
if _, ok := desiredMap[domain]; !ok {
|
||||
fmt.Printf("Lösche: %s : %s\n", domain, ip)
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func (c *AdGuardClient) ApplyRewrites(desired []Rewrite) error {
|
||||
existing, err := c.GetRewrites()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingMap := make(map[string]string)
|
||||
for _, r := range existing {
|
||||
existingMap[r.Domain] = r.Answer
|
||||
}
|
||||
|
||||
desiredMap := make(map[string]string)
|
||||
for _, r := range desired {
|
||||
desiredMap[r.Domain] = r.Answer
|
||||
}
|
||||
|
||||
for domain, ip := range desiredMap {
|
||||
if existingIP, ok := existingMap[domain]; ok {
|
||||
if existingIP != ip {
|
||||
if err := c.UpdateRewrite(Rewrite{Domain: domain, Answer: existingIP}, Rewrite{Domain: domain, Answer: ip}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := c.AddRewrite(Rewrite{Domain: domain, Answer: ip}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for domain, ip := range existingMap {
|
||||
if _, ok := desiredMap[domain]; !ok {
|
||||
if err := c.DeleteRewrite(Rewrite{Domain: domain, Answer: ip}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"domain": "adguard01.home",
|
||||
"answer": "192.168.178.117"
|
||||
},
|
||||
{
|
||||
"domain": "ipmi.home",
|
||||
"answer": "192.168.178.126"
|
||||
},
|
||||
{
|
||||
"domain": "docker-server.home",
|
||||
"answer": "192.168.178.21"
|
||||
},
|
||||
{
|
||||
"domain": "tdarr.home",
|
||||
"answer": "192.168.178.106"
|
||||
},
|
||||
{
|
||||
"domain": "arr.home",
|
||||
"answer": "192.168.178.121"
|
||||
},
|
||||
{
|
||||
"domain": "gitlab.home",
|
||||
"answer": "192.168.178.111"
|
||||
},
|
||||
{
|
||||
"domain": "share-box.home",
|
||||
"answer": "192.168.178.123"
|
||||
},
|
||||
{
|
||||
"domain": "pve01.home",
|
||||
"answer": "192.168.178.141"
|
||||
},
|
||||
{
|
||||
"domain": "adguard02.home",
|
||||
"answer": "192.168.178.105"
|
||||
},
|
||||
{
|
||||
"domain": "gitea.home",
|
||||
"answer": "192.168.178.116"
|
||||
},
|
||||
{
|
||||
"domain": "minecraft.home",
|
||||
"answer": "192.168.178.119"
|
||||
},
|
||||
{
|
||||
"domain": "pve03.home",
|
||||
"answer": "192.168.178.189"
|
||||
},
|
||||
{
|
||||
"domain": "vaultwarden.home",
|
||||
"answer": "192.168.178.113"
|
||||
},
|
||||
{
|
||||
"domain": "w11.home",
|
||||
"answer": "192.168.178.133"
|
||||
},
|
||||
{
|
||||
"domain": "jellyfin.home",
|
||||
"answer": "192.168.178.192"
|
||||
},
|
||||
{
|
||||
"domain": "grafana.home",
|
||||
"answer": "192.168.178.118"
|
||||
},
|
||||
{
|
||||
"domain": "paperless.home",
|
||||
"answer": "192.168.178.126"
|
||||
},
|
||||
{
|
||||
"domain": "printer.home",
|
||||
"answer": "192.168.178.57"
|
||||
},
|
||||
{
|
||||
"domain": "opencloud.home",
|
||||
"answer": "192.168.178.112"
|
||||
},
|
||||
{
|
||||
"domain": "unifi-controller.home",
|
||||
"answer": "192.168.178.110"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
rewrites.yaml
Normal file
34
rewrites.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
rewrites:
|
||||
- domain: pve01.home
|
||||
answer: 192.168.178.141
|
||||
- domain: share-box.home
|
||||
answer: 192.168.178.123
|
||||
- domain: pve02.home
|
||||
answer: 192.168.178.20
|
||||
- domain: pve03.home
|
||||
answer: 192.168.178.189
|
||||
- domain: vaultwarden.home
|
||||
answer: 192.168.178.113
|
||||
- domain: tdarr.home
|
||||
answer: 192.168.178.106
|
||||
- domain: gitlab.home
|
||||
answer: 192.168.178.111
|
||||
- domain: jellyfin.home
|
||||
answer: 192.168.178.192
|
||||
- domain: adguard01.home
|
||||
answer: 192.168.178.117
|
||||
- domain: ipmi.home
|
||||
answer: 192.168.178.126
|
||||
- domain: docker-server.home
|
||||
answer: 192.168.178.21
|
||||
- domain: arr.home
|
||||
answer: 192.168.178.121
|
||||
- domain: minecraft.home
|
||||
answer: 192.168.178.119
|
||||
- domain: gitea.home
|
||||
answer: 192.168.178.116
|
||||
- domain: adguard02.home
|
||||
answer: 192.168.178.105
|
||||
- domain: jellyfin.home
|
||||
answer: 192.168.178.124
|
||||
46
utils.go
Normal file
46
utils.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func LoadEnv() {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
fmt.Println("Warnung: .env Datei konnte nicht geladen werden.")
|
||||
}
|
||||
}
|
||||
|
||||
func LoadYAML() ([]Rewrite, error) {
|
||||
file := os.Getenv("YAML_FILE")
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var data struct {
|
||||
Rewrites []Rewrite `yaml:"rewrites"`
|
||||
}
|
||||
err = yaml.NewDecoder(f).Decode(&data)
|
||||
return data.Rewrites, err
|
||||
}
|
||||
|
||||
func SaveYAML(rewrites []Rewrite) error {
|
||||
file := os.Getenv("YAML_FILE")
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data := map[string][]Rewrite{
|
||||
"rewrites": rewrites,
|
||||
}
|
||||
enc := yaml.NewEncoder(f)
|
||||
enc.SetIndent(2)
|
||||
return enc.Encode(data)
|
||||
}
|
||||
Reference in New Issue
Block a user