change yaml to json

This commit is contained in:
2025-11-06 23:30:13 +01:00
parent be984bd6cf
commit abe64963ef
6 changed files with 423 additions and 216 deletions

View File

@@ -1,73 +0,0 @@
# .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

View File

@@ -1,22 +0,0 @@
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
View File

@@ -168,3 +168,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
.claude/

387
main.py
View File

@@ -1,131 +1,382 @@
import requests import requests
import yaml import json
import os import os
import argparse
import sys
# from dotenv import load_dotenv from dotenv import load_dotenv
# 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)
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")
# 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") url = os.getenv("ADGUARD_URL")
username = os.getenv("ADGUARD_USER") username = os.getenv("ADGUARD_USER")
password = os.getenv("ADGUARD_PASSWORD") password = os.getenv("ADGUARD_PASSWORD")
yaml_file = os.getenv("YAML_FILE") 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"AdGuard URL: {url}")
print(f"Username: {username}") print(f"Username: {username}")
print(f"JSON File: {json_file}")
session = requests.Session() 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")
class IndentDumper(yaml.Dumper): if "rewrites" not in data:
def increase_indent(self, flow=False, indentless=False): raise ValueError("JSON must contain a 'rewrites' key")
return super().increase_indent(flow, False)
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
def login(): def login():
r = session.post(f"{url}/control/login", json={"name": username, "password": password}) """Login to AdGuard API."""
try:
r = session.post(f"{url}/control/login", json={"name": username, "password": password}, timeout=10)
if r.status_code != 200: if r.status_code != 200:
raise Exception(f"Login fehlgeschlagen: {r.status_code} {r.text}") print(f"ERROR: Login failed with status {r.status_code}")
print(f"Response: {r.text}")
sys.exit(1)
print("Login erfolgreich.") 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)
def load_yaml(): def load_json():
with open(yaml_file, "r") as f: """Load and validate JSON file."""
return yaml.safe_load(f)["rewrites"] 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 save_yaml(data): try:
with open(yaml_file, "w") as f: with open(json_file, "r") as f:
yaml.dump( 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}, {"rewrites": data},
f, f,
Dumper=IndentDumper, indent=2
default_flow_style=False,
sort_keys=False
) )
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 get_rewrites(): def get_rewrites():
r = session.get(f"{url}/control/rewrite/list") """Get current rewrites from AdGuard."""
try:
r = session.get(f"{url}/control/rewrite/list", timeout=10)
r.raise_for_status() r.raise_for_status()
print(r.json()) data = r.json()
return r.json() # AdGuard gibt direkt eine Liste zurück 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)
def add_rewrite(entry): def add_rewrite(entry):
r = session.post(f"{url}/control/rewrite/add", json=entry) """Add a rewrite entry to AdGuard."""
try:
r = session.post(f"{url}/control/rewrite/add", json=entry, timeout=10)
r.raise_for_status() r.raise_for_status()
print(f"Hinzugefügt: {entry}") 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
def delete_rewrite(entry): def delete_rewrite(entry):
r = session.post(f"{url}/control/rewrite/delete", json=entry) """Delete a rewrite entry from AdGuard."""
try:
r = session.post(f"{url}/control/rewrite/delete", json=entry, timeout=10)
r.raise_for_status() r.raise_for_status()
print(f"Gelöscht: {entry}") 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
def update_rewrite(old_entry, new_entry): def update_rewrite(old_entry, new_entry):
r = session.put(f"{url}/control/rewrite/update", json={"target": old_entry, "update": 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() r.raise_for_status()
print(f"Aktualisiert: {old_entry} -> {new_entry}") 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
def merge_rewrites(existing, desired): def sync_to_adguard(desired):
# Mapping: domain -> IP (bestehend) """Sync desired rewrites to AdGuard."""
existing_map = {r["domain"]: r["answer"] for r in existing} print("Fetching current rewrites from AdGuard...")
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 = get_rewrites()
existing_map = {r["domain"]: r["answer"] for r in existing} existing_map = {r["domain"]: r["answer"] for r in existing}
desired_map = {r["domain"]: r["answer"] for r in desired} desired_map = {r["domain"]: r["answer"] for r in desired}
failed_operations = []
# Änderungen oder Neueinträge # Änderungen oder Neueinträge
updates_count = 0
additions_count = 0
for domain, ip in desired_map.items(): for domain, ip in desired_map.items():
if domain in existing_map: if domain in existing_map:
if existing_map[domain] != ip: if existing_map[domain] != ip:
update_rewrite( if not update_rewrite(
{"domain": domain, "answer": existing_map[domain]}, {"domain": domain, "answer": existing_map[domain]},
{"domain": domain, "answer": ip} {"domain": domain, "answer": ip}
) ):
failed_operations.append(f"Update {domain}")
else: else:
add_rewrite({"domain": domain, "answer": ip}) updates_count += 1
else:
if not add_rewrite({"domain": domain, "answer": ip}):
failed_operations.append(f"Add {domain}")
else:
additions_count += 1
# Entferne veraltete # Entferne veraltete
deletions_count = 0
for domain, ip in existing_map.items(): for domain, ip in existing_map.items():
if domain not in desired_map: if domain not in desired_map:
delete_rewrite({"domain": domain, "answer": ip}) 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}")
def main(): 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"
)
try:
args = parser.parse_args()
except SystemExit:
raise
# 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.")
# 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() login()
current = get_rewrites()
desired = load_yaml()
# Merge existing and desired rewrites if args.sync:
merged = merge_rewrites(current, desired) # Sync from JSON to AdGuard (JSON is source of truth)
print("\n=== Starting sync from JSON to AdGuard ===")
desired = load_json()
# Save merged rewrites back to YAML if len(desired) == 0:
save_yaml(merged) print("WARNING: No rewrites found in JSON file. Nothing to sync.")
sys.exit(0)
# Apply the merged rewrites to AdGuard success = sync_to_adguard(desired)
apply_rewrites(merged)
print("Alle Änderungen wurden erfolgreich angewendet.") 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)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

84
rewrites.json Normal file
View File

@@ -0,0 +1,84 @@
{
"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"
}
]
}

View File

@@ -1,34 +0,0 @@
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