pihole-opnsense-sync/syncer.py

109 lines
4.2 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import logging
import time
from datetime import datetime
from pathlib import Path
import requests
import urllib3
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
urllib3.disable_warnings()
class WatchdogHandler(FileSystemEventHandler):
def __init__(self, args):
self.args = args
def on_modified(self, event):
if event.src_path == args.custom_path:
file_modified(args)
def file_modified(args):
response = requests.get(args.opnsense + "/api/unbound/settings/searchHostOverride", auth=(args.api_key, args.api_secret), verify=not args.insecure)
if response.status_code == 200:
data = response.json()
current_overrides = set([(x['server'], x['domain']) for x in data.get('rows', []) if x['description'] != 'SAVE'])
current_overrides_uuid = {x['domain']: x for x in data.get('rows', [])}
pihole_custom = set([(x[0], x[1]) for x in [y.split(' ') for y in Path(args.custom_path).read_text().split('\n')] if len(x) > 1])
to_remove = current_overrides - pihole_custom
to_add = pihole_custom - current_overrides
if not len(to_remove) and not len(to_add):
logger.info('No changes.')
return
for item_ip, item_domain in to_remove:
item_data = current_overrides_uuid[item_domain]
p = requests.post(args.opnsense + f"/api/unbound/settings/delHostOverride/{item_data['uuid']}", auth=(args.api_key, args.api_secret), verify=not args.insecure)
if p.status_code != 200:
raise Exception(f'Failed to delete item {item_domain}: {p.status_code} - {p.text}')
else:
logger.info(f'Deleted: {item_domain}')
now = datetime.now()
dt_string = now.strftime("%m/%d/%Y %H:%M:%S")
for item_ip, item_domain in to_add:
p_data = {
'host': {
'enabled': '1',
'hostname': '',
'domain': item_domain,
'rr': 'A',
'mxprio': '',
'mx': '',
'server': item_ip,
'description': f'Synced from Pi-hole {dt_string}'
}
}
p = requests.post(args.opnsense + "/api/unbound/settings/addHostOverride", json=p_data, auth=(args.api_key, args.api_secret), verify=not args.insecure)
if p.status_code != 200:
raise Exception(f'Failed to add item {item_domain}: {p.status_code} - {p.text}')
else:
logger.info(f'Added: {item_domain}')
p_reload = requests.post(args.opnsense + "/api/unbound/service/reconfigure", auth=(args.api_key, args.api_secret), verify=not args.insecure)
if p_reload.status_code != 200:
raise Exception(f'Failed to reload service: {p_reload.status_code} - {p_reload.text}')
else:
logger.info(f'Unbound reloaded.')
else:
raise Exception(f"Failed to fetch host overrides: {response.status_code} - {response.text}")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Sync custom DNS entries in Pi-hole to OPNsense Unbound.')
parser.add_argument('--opnsense', required=True, help='Address of your OPNsense server. Example: https://192.168.1.1')
parser.add_argument('--custom-path', default='/etc/pihole/custom.list', help="Path to Pi-hole's custom.list. Default: /etc/pihole/custom.list")
parser.add_argument('--api-key', required=True, help='OPNsense API key.')
parser.add_argument('--api-secret', required=True, help='OPNsense API secret.')
parser.add_argument('--insecure', action='store_true', help="Don't verify SSL.")
args = parser.parse_args()
logging.basicConfig()
logger = logging.getLogger('syncer')
logger.setLevel(logging.INFO)
logger.info('Running initial sync...')
file_modified(args)
event_handler = WatchdogHandler(args)
observer = Observer()
observer.schedule(event_handler, path='/etc/pihole/', recursive=False)
observer.start()
logger.info('Started watchdog.')
try:
while True:
time.sleep(100)
except KeyboardInterrupt:
observer.stop()
observer.join()