Initial commit

This commit is contained in:
Nik Rozman
2023-07-24 22:02:37 +02:00
commit 3f29f7eaa6
6 changed files with 314 additions and 0 deletions

68
backend/hetzner.py Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

1
zones.ini Normal file
View File

@@ -0,0 +1 @@
[4.3.2.1.in-addr.arpa.]