Initial commit
This commit is contained in:
68
backend/hetzner.py
Normal file
68
backend/hetzner.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import configparser
|
||||
import requests
|
||||
|
||||
|
||||
def reverse_ip_to_standard(reverse_ip):
|
||||
parts = reverse_ip.split(".")
|
||||
parts = parts[:4]
|
||||
standard_ip = ".".join(reversed(parts))
|
||||
|
||||
return standard_ip
|
||||
|
||||
|
||||
class HetznerBackend:
|
||||
def __init__(self, config_file='config.ini'):
|
||||
self.config_file = config_file
|
||||
self.username = None
|
||||
self.password = None
|
||||
|
||||
self.load_credentials()
|
||||
|
||||
def load_credentials(self):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(self.config_file)
|
||||
|
||||
if "Hetzner" not in config:
|
||||
raise ValueError("Hetzner section not found in config.ini")
|
||||
|
||||
self.username = config["Hetzner"].get("username")
|
||||
self.password = config["Hetzner"].get("password")
|
||||
|
||||
if not self.username or not self.password:
|
||||
raise ValueError("Hetzner username or password not provided in config.ini")
|
||||
|
||||
def update_record(self, reverse_ip, record):
|
||||
ip = reverse_ip_to_standard(reverse_ip)
|
||||
|
||||
# Check if the record contains the necessary data
|
||||
if not record or "content" not in record[0]:
|
||||
print("Invalid record data. 'content' field is missing.")
|
||||
return None
|
||||
|
||||
domain = record[0]["content"].strip(".")
|
||||
|
||||
data = {"ptr": domain}
|
||||
response = requests.post(f"https://robot-ws.your-server.de/rdns/{ip}",
|
||||
data=data, auth=(self.username, self.password))
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Updated RDNS record for: " + ip + " to: " + record[0]["content"])
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Failed to fetch rDNS data. Status code: {response.status_code}")
|
||||
return None
|
||||
|
||||
def create_record(self, reverse_ip, record):
|
||||
# Hetzner's API treats creation and updates the same way so just call update_record instead
|
||||
self.update_record(reverse_ip, record)
|
||||
pass
|
||||
|
||||
def delete_record(self, reverse_ip):
|
||||
ip = reverse_ip_to_standard(reverse_ip)
|
||||
response = requests.delete(f"https://robot-ws.your-server.de/rdns/{ip}", auth=(self.username, self.password))
|
||||
|
||||
if response.status_code == 200:
|
||||
print("Deleted RDNS record for: " + ip)
|
||||
else:
|
||||
print(f"Failed to delete rDNS record. Status code: {response.status_code}")
|
||||
return None
|
||||
44
backend/ovh.py
Normal file
44
backend/ovh.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import configparser
|
||||
import requests
|
||||
|
||||
|
||||
def reverse_ip_to_standard(reverse_ip):
|
||||
parts = reverse_ip.split(".")
|
||||
parts = parts[:4]
|
||||
standard_ip = ".".join(reversed(parts))
|
||||
|
||||
return standard_ip
|
||||
|
||||
|
||||
class OvhBackend:
|
||||
def __init__(self, config_file='config.ini'):
|
||||
self.config_file = config_file
|
||||
self.username = None
|
||||
self.password = None
|
||||
|
||||
self.load_credentials()
|
||||
|
||||
def load_credentials(self):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(self.config_file)
|
||||
|
||||
if "OVH" not in config:
|
||||
raise ValueError("OVH section not found in config.ini")
|
||||
|
||||
self.username = config["OVH"].get("username")
|
||||
self.password = config["OVH"].get("password")
|
||||
|
||||
if not self.username or not self.password:
|
||||
raise ValueError("OVH username or password not provided in config.ini")
|
||||
|
||||
def update_record(self, reverse_ip, record):
|
||||
# Do update record stuff here
|
||||
pass
|
||||
|
||||
def create_record(self, reverse_ip, record):
|
||||
# Do create record stuff here
|
||||
pass
|
||||
|
||||
def delete_record(self, reverse_ip):
|
||||
# Do delete record stuff here
|
||||
pass
|
||||
12
config.ini
Normal file
12
config.ini
Normal file
@@ -0,0 +1,12 @@
|
||||
[PowerDNS]
|
||||
api_endpoint=https://x/api/v1/servers/localhost
|
||||
api_key=x
|
||||
frequency=5
|
||||
|
||||
[Hetzner]
|
||||
username=x
|
||||
password=x
|
||||
|
||||
[OVH]
|
||||
username=x
|
||||
password=x
|
||||
189
main.py
Normal file
189
main.py
Normal file
@@ -0,0 +1,189 @@
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
import configparser
|
||||
import schedule
|
||||
import time
|
||||
from backend.hetzner import HetznerBackend
|
||||
from backend.ovh import OvhBackend
|
||||
|
||||
|
||||
def compare_zone_records(saved_records, new_records, backend_type):
|
||||
saved_records_dict = {}
|
||||
for record in saved_records["rrsets"]:
|
||||
saved_records_dict[record["name"]] = record["records"]
|
||||
|
||||
new_records_dict = {}
|
||||
for record in new_records["rrsets"]:
|
||||
new_records_dict[record["name"]] = record["records"]
|
||||
|
||||
if backend_type == "hetzner":
|
||||
backend = HetznerBackend()
|
||||
elif backend_type == "ovh":
|
||||
backend = OvhBackend()
|
||||
else:
|
||||
print(f"Specified backend not supported: {backend_type}")
|
||||
return
|
||||
|
||||
for name, new_records_list in new_records_dict.items():
|
||||
if name not in saved_records_dict:
|
||||
# Do create here
|
||||
backend.create_record(name, new_records_list) # Pass the name along with the content
|
||||
|
||||
elif new_records_list != saved_records_dict[name]:
|
||||
# Do update here
|
||||
backend.update_record(name, new_records_list) # Pass the name along with the content
|
||||
|
||||
for name, saved_records_list in saved_records_dict.items():
|
||||
if name not in new_records_dict:
|
||||
# Do delete here
|
||||
backend.delete_record(name) # Pass the name to the delete_record method
|
||||
|
||||
|
||||
class PowerDNSProxy:
|
||||
def __init__(self, config_file='config.ini', zones_file='zones.ini'):
|
||||
self.api_url = None
|
||||
self.api_key = None
|
||||
self.config_file = config_file
|
||||
self.freq = 5 # Default frequency
|
||||
self.zones_with_backend = {} # Dictionary to store zones with their backend
|
||||
self.zones_file = zones_file
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
def load_config(self):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(self.config_file)
|
||||
|
||||
if "PowerDNS" not in config:
|
||||
print("PowerDNS section not found in config.ini")
|
||||
return False
|
||||
else:
|
||||
self.api_url = config["PowerDNS"]["api_endpoint"]
|
||||
self.api_key = config["PowerDNS"]["api_key"]
|
||||
|
||||
if not self.api_url or not self.api_key:
|
||||
print("API URL or API Key not provided. Please check the configuration.")
|
||||
return False
|
||||
|
||||
self.freq = int(config["PowerDNS"].get("frequency", 5)) # Set the frequency from config
|
||||
self.zones_with_backend = self.read_zones_from_config() # Call read_zones_from_config here
|
||||
return True
|
||||
|
||||
def main(self):
|
||||
if not self.load_config():
|
||||
return
|
||||
|
||||
zones = self.get_zones()
|
||||
if type(zones) is int:
|
||||
print("Error getting zones: http code " + str(zones))
|
||||
return
|
||||
|
||||
tracked_zones = {}
|
||||
for zone in zones:
|
||||
zone_name = zone["name"]
|
||||
if zone_name in self.zones_with_backend: # Use self.zones_with_backend instead of config_zones
|
||||
tracked_zones[zone_name] = {
|
||||
"id": zone["id"],
|
||||
"serial": zone["serial"],
|
||||
"backend": self.zones_with_backend[zone_name]["backend"]
|
||||
}
|
||||
|
||||
print("Zones tracked: " + str(len(tracked_zones)))
|
||||
|
||||
with open("states.json", "w") as f:
|
||||
json.dump(tracked_zones, f, indent=4)
|
||||
|
||||
print("States saved to states.json")
|
||||
|
||||
# Define the scheduled job function
|
||||
def scheduled_job():
|
||||
self.check_zone_serial()
|
||||
|
||||
# Schedule the job to run every 'freq' seconds
|
||||
schedule.every(self.freq).seconds.do(scheduled_job)
|
||||
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
|
||||
def get_zones(self):
|
||||
headers = {"X-API-Key": self.api_key}
|
||||
response = requests.get(self.api_url + "/zones", headers=headers)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
return response.status_code
|
||||
|
||||
def read_zones_from_config(self):
|
||||
config = configparser.ConfigParser(allow_no_value=True)
|
||||
config.read(self.zones_file)
|
||||
|
||||
zones_with_backend = {}
|
||||
|
||||
for section in config.sections():
|
||||
backend = "default" # Initialize the backend variable
|
||||
if "backend" in config[section]:
|
||||
backend = config[section]["backend"].strip('"') # Remove any quotes around the backend value
|
||||
zones_with_backend[section.strip()] = {"backend": backend}
|
||||
|
||||
return zones_with_backend
|
||||
|
||||
def get_zone_records(self, zone_id):
|
||||
headers = {"X-API-Key": self.api_key}
|
||||
response = requests.get(self.api_url + f"/zones/{zone_id}", headers=headers)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Error getting zone records: http code {response.status_code}")
|
||||
return None
|
||||
|
||||
def save_zone_records(self, zone_data, backend):
|
||||
zone_name = zone_data["name"]
|
||||
zone_id = zone_data["id"]
|
||||
|
||||
records = self.get_zone_records(zone_id)
|
||||
if records is not None:
|
||||
states_dir = "states"
|
||||
if not os.path.exists(states_dir):
|
||||
os.makedirs(states_dir)
|
||||
|
||||
filename = os.path.join(states_dir, f"{zone_name}.json")
|
||||
|
||||
try:
|
||||
with open(filename, "r") as f:
|
||||
saved_records = json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
print(f"File {filename} not found or invalid, creating new file")
|
||||
saved_records = []
|
||||
|
||||
with open(filename, "w") as f:
|
||||
json.dump(records, f, indent=4)
|
||||
|
||||
print(f"Zone records for {zone_name} saved to {filename}")
|
||||
compare_zone_records(saved_records, records, backend)
|
||||
else:
|
||||
print(f"Error getting zone records for {zone_name}")
|
||||
|
||||
def check_zone_serial(self):
|
||||
zones = self.get_zones()
|
||||
# Open states.json
|
||||
with open("states.json", "r") as f:
|
||||
tracked_zones = json.load(f)
|
||||
|
||||
print("Checking zone serials from saved states")
|
||||
|
||||
for zone in zones:
|
||||
if zone["name"] in tracked_zones:
|
||||
if tracked_zones[zone["name"]]["serial"] != zone["serial"]:
|
||||
print("Zone " + zone["name"] + " has changed!")
|
||||
self.save_zone_records(zone, tracked_zones[zone["name"]]["backend"])
|
||||
tracked_zones[zone["name"]]["serial"] = zone["serial"]
|
||||
|
||||
# Update the state.json file with the updated tracked_zones data
|
||||
with open("states.json", "w") as f:
|
||||
json.dump(tracked_zones, f, indent=4)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pdns_proxy = PowerDNSProxy()
|
||||
pdns_proxy.main()
|
||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
Reference in New Issue
Block a user