Add update_ddns.py
This commit is contained in:
commit
d40793074f
153
update_ddns.py
Normal file
153
update_ddns.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env python3
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
# --- KONFIGURACJA ---
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
|
||||
DOMAINS_FILE = os.path.join(BASE_DIR, "domains.txt")
|
||||
LOG_FILE = "/var/log/ionos-ddns.log"
|
||||
INTERVAL = 600 # Czas w sekundach (10 minut)
|
||||
|
||||
# --- LOGOWANIE ---
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
handlers=[
|
||||
logging.FileHandler(LOG_FILE),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
|
||||
class IonosClient:
|
||||
def __init__(self, prefix, secret):
|
||||
self.base_url = "https://api.hosting.ionos.com/dns/v1"
|
||||
self.headers = {
|
||||
"X-API-Key": f"{prefix}.{secret}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
def get_zones(self):
|
||||
try:
|
||||
resp = requests.get(f"{self.base_url}/zones", headers=self.headers)
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
logging.error(f"API Error (get_zones): {resp.status_code} {resp.text}")
|
||||
except Exception as e:
|
||||
logging.error(f"Connection Error (get_zones): {e}")
|
||||
return []
|
||||
|
||||
def get_zone_records(self, zone_id):
|
||||
try:
|
||||
resp = requests.get(f"{self.base_url}/zones/{zone_id}", headers=self.headers)
|
||||
if resp.status_code == 200:
|
||||
return resp.json().get('records', [])
|
||||
except Exception as e:
|
||||
logging.error(f"Connection Error (get_zone_records): {e}")
|
||||
return []
|
||||
|
||||
def create_record(self, zone_id, name, ip):
|
||||
data = [{"name": name, "type": "A", "content": ip, "ttl": 3600}]
|
||||
logging.info(f" [ACTION] CREATING: {name} -> {ip}")
|
||||
try:
|
||||
resp = requests.post(f"{self.base_url}/zones/{zone_id}/records", headers=self.headers, json=data)
|
||||
if resp.status_code == 201:
|
||||
logging.info(" [SUCCESS] Record created.")
|
||||
else:
|
||||
logging.error(f" [ERROR] API: {resp.text}")
|
||||
except Exception as e:
|
||||
logging.error(f" [ERROR] Connection: {e}")
|
||||
|
||||
def update_record(self, zone_id, record_id, name, ip):
|
||||
data = {"name": name, "type": "A", "content": ip, "ttl": 3600}
|
||||
logging.info(f" [ACTION] UPDATING: {name} -> {ip}")
|
||||
try:
|
||||
resp = requests.put(f"{self.base_url}/zones/{zone_id}/records/{record_id}", headers=self.headers, json=data)
|
||||
if resp.status_code == 200:
|
||||
logging.info(" [SUCCESS] Updated.")
|
||||
else:
|
||||
logging.error(f" [ERROR] API: {resp.text}")
|
||||
except Exception as e:
|
||||
logging.error(f" [ERROR] Connection: {e}")
|
||||
|
||||
def load_config():
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
logging.error(f"CRITICAL: Config file missing: {CONFIG_FILE}")
|
||||
return None
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logging.error(f"CRITICAL: Config read error: {e}")
|
||||
return None
|
||||
|
||||
def get_public_ip(url):
|
||||
try:
|
||||
return requests.get(url, timeout=10).text.strip()
|
||||
except Exception as e:
|
||||
logging.error(f"Public IP fetch error: {e}")
|
||||
return None
|
||||
|
||||
def find_zone_for_domain(full_domain, zones):
|
||||
best_match = None
|
||||
best_len = 0
|
||||
for zone in zones:
|
||||
zone_name = zone['name']
|
||||
if full_domain == zone_name or full_domain.endswith("." + zone_name):
|
||||
if len(zone_name) > best_len:
|
||||
best_len = len(zone_name)
|
||||
best_match = zone
|
||||
return best_match
|
||||
|
||||
def main():
|
||||
logging.info("=== STARTING IONOS DDNS SERVICE ===")
|
||||
|
||||
config = load_config()
|
||||
if not config:
|
||||
sys.exit(1)
|
||||
|
||||
ionos = IonosClient(config['api_prefix'], config['api_secret'])
|
||||
|
||||
while True:
|
||||
try:
|
||||
if not os.path.exists(DOMAINS_FILE):
|
||||
logging.error(f"Domains file missing: {DOMAINS_FILE}")
|
||||
else:
|
||||
with open(DOMAINS_FILE, 'r') as f:
|
||||
target_domains = [line.strip() for line in f if line.strip()]
|
||||
|
||||
public_ip = get_public_ip(config['check_ip_url'])
|
||||
|
||||
if public_ip and target_domains:
|
||||
zones = ionos.get_zones()
|
||||
if zones:
|
||||
for full_domain in target_domains:
|
||||
zone = find_zone_for_domain(full_domain, zones)
|
||||
if not zone:
|
||||
logging.warning(f" No zone found for: {full_domain}")
|
||||
continue
|
||||
|
||||
records = ionos.get_zone_records(zone['id'])
|
||||
target_record = next((r for r in records if r['name'] == full_domain and r['type'] == 'A'), None)
|
||||
|
||||
if target_record:
|
||||
if target_record['content'] != public_ip:
|
||||
ionos.update_record(zone['id'], target_record['id'], full_domain, public_ip)
|
||||
else:
|
||||
# Opcjonalnie można zakomentować, żeby nie spamowało logów co 10 min
|
||||
logging.info(f" [OK] {full_domain} is up to date.")
|
||||
else:
|
||||
ionos.create_record(zone['id'], full_domain, public_ip)
|
||||
except Exception as e:
|
||||
logging.critical(f"UNEXPECTED LOOP ERROR: {e}")
|
||||
|
||||
time.sleep(INTERVAL)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user