diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..6cc5c2a --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,70 @@ +# .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: Set up Python 3.11 + run: | + sudo apt update + sudo apt install python3.11 python3-pip + + - 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 + + 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: Set up Python 3.11 + run: | + sudo apt update + sudo apt install python3.11 python3-pip + + - 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 \ No newline at end of file diff --git a/README.md b/README.md index eebde8a..d9dccc3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ -# adguard-dns-tools +# AdGuard DNS Rewrite Sync Tool +This script synchronizes DNS rewrite rules between a local YAML configuration and an AdGuard Home instance using its REST API. + +## Features + +- Logs into AdGuard Home via its control API +- Loads desired DNS rewrite entries from a YAML file +- Retrieves current DNS rewrite rules from AdGuard +- Compares current vs. desired state and: + - Adds new entries + - Updates existing ones with changed IPs + - Deletes entries no longer needed +- Merges local and remote rules and writes back to the YAML file + +## Requirements + +- Python 3.7+ +- [`requests`](https://pypi.org/project/requests/) +- [`pyyaml`](https://pypi.org/project/PyYAML/) +- [`python-dotenv`](https://pypi.org/project/python-dotenv/) + +Install dependencies: + +```bash +pip install -r requirements.txt + +## Configuration +Create a .env file in the same directory with the following variables: + +```env +ADGUARD_URL=http://:3000 +ADGUARD_USER=admin +PASSWORD=yourpassword +YAML_FILE=rewrites.yaml +``` + +# YAML File Format +The YAML file should contain rewrite entries under the rewrites key: + +```yaml +rewrites: + - domain: "example.org" + answer: "127.0.0.1" + - domain: "another.example.org" + answer: "192.168.1.1" +``` + +# Usage +Run the script: + +```bash +python main.py +``` \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..e89bb50 --- /dev/null +++ b/main.py @@ -0,0 +1,131 @@ +import requests +import yaml +import os + +# from dotenv import load_dotenv + +# load_dotenv() + +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}") + +session = requests.Session() + +class IndentDumper(yaml.Dumper): + def increase_indent(self, flow=False, indentless=False): + return super().increase_indent(flow, False) + +def login(): + 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_yaml(): + with open(yaml_file, "r") as f: + return yaml.safe_load(f)["rewrites"] + +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(): + 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): + r = session.post(f"{url}/control/rewrite/add", json=entry) + r.raise_for_status() + print(f"Hinzugefügt: {entry}") + +def delete_rewrite(entry): + 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): + 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 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} + + # Änderungen oder Neueinträge + for domain, ip in desired_map.items(): + if domain in existing_map: + if existing_map[domain] != ip: + update_rewrite( + {"domain": domain, "answer": existing_map[domain]}, + {"domain": domain, "answer": ip} + ) + else: + add_rewrite({"domain": domain, "answer": ip}) + + # Entferne veraltete + for domain, ip in existing_map.items(): + if domain not in desired_map: + delete_rewrite({"domain": domain, "answer": ip}) + +def main(): + login() + current = get_rewrites() + desired = load_yaml() + + # Merge existing and desired rewrites + merged = merge_rewrites(current, desired) + + # Save merged rewrites back to YAML + save_yaml(merged) + + # Apply the merged rewrites to AdGuard + apply_rewrites(merged) + print("Alle Änderungen wurden erfolgreich angewendet.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/rewrites.yaml b/rewrites.yaml new file mode 100644 index 0000000..3a006f4 --- /dev/null +++ b/rewrites.yaml @@ -0,0 +1,30 @@ + +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 \ No newline at end of file