feat: add cloudflare module for dynamic dns updates

This commit is contained in:
Fredrick Amnehagen 2026-02-05 19:15:50 +01:00
parent ce67360c3c
commit 837bafba09
3 changed files with 95 additions and 0 deletions

73
infra_cli/cloudflare.py Normal file
View file

@ -0,0 +1,73 @@
import requests
import sys
class CloudflareManager:
def __init__(self, config):
self.token = config.get('cloudflare.token')
self.ddns_domains = config.get('cloudflare.ddns_domains', [])
self.api_url = "https://api.cloudflare.com/client/v4"
self.headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
def get_external_ip(self):
try:
return requests.get("https://checkip.amazonaws.com").text.strip()
except Exception as e:
print(f"Error fetching external IP: {e}")
return None
def get_zone_id(self, domain):
res = requests.get(f"{self.api_url}/zones?name={domain}", headers=self.headers)
data = res.json()
if data.get('success') and data['result']:
return data['result'][0]['id']
return None
def update_ddns(self, force=False):
current_ip = self.get_external_ip()
if not current_ip:
return "Failed to determine current external IP."
results = []
for domain in self.ddns_domains:
zone_id = self.get_zone_id(domain)
if not zone_id:
results.append(f"[{domain}] Zone not found.")
continue
# Find A record for the root domain
res = requests.get(f"{self.api_url}/zones/{zone_id}/dns_records?type=A&name={domain}", headers=self.headers)
records = res.json().get('result', [])
if not records:
# Create if missing? For now, just report
results.append(f"[{domain}] No A record found to update.")
continue
record = records[0]
if record['content'] == current_ip and not force:
results.append(f"[{domain}] Already up to date ({current_ip}).")
continue
# Update record
update_data = {
"type": "A",
"name": domain,
"content": current_ip,
"ttl": 1, # Auto
"proxied": record.get('proxied', True)
}
u_res = requests.put(f"{self.api_url}/zones/{zone_id}/dns_records/{record['id']}",
headers=self.headers, json=update_data)
if u_res.json().get('success'):
results.append(f"[{domain}] Updated to {current_ip}.")
else:
results.append(f"[{domain}] Update failed: {u_res.text}")
return "\n".join(results)
def list_domains(self):
return self.ddns_domains

View file

@ -5,6 +5,7 @@ from .ingress import IngressManager
from .router import RouterManager from .router import RouterManager
from .proxmox import ProxmoxManager from .proxmox import ProxmoxManager
from .samba import SambaManager from .samba import SambaManager
from .cloudflare import CloudflareManager
import sys import sys
@click.group() @click.group()
@ -18,6 +19,26 @@ def cli(ctx, config):
click.echo(f"Error: {e}", err=True) click.echo(f"Error: {e}", err=True)
sys.exit(1) sys.exit(1)
@cli.group()
def cloudflare():
"""Manage Cloudflare DNS and DDNS"""
pass
@cloudflare.command(name='list-ddns')
@click.pass_obj
def cf_list_ddns(config):
mgr = CloudflareManager(config)
for domain in mgr.list_domains():
click.echo(domain)
@cloudflare.command(name='update-ddns')
@click.option('--force', is_flag=True, help='Force update even if IP matches')
@click.pass_obj
def cf_update_ddns(config, force):
mgr = CloudflareManager(config)
click.echo("Updating Cloudflare DDNS records...")
click.echo(mgr.update_ddns(force))
@cli.group() @cli.group()
def proxmox(): def proxmox():
"""Manage Proxmox VMs and Containers""" """Manage Proxmox VMs and Containers"""

View file

@ -7,6 +7,7 @@ setup(
install_requires=[ install_requires=[
"click", "click",
"pyyaml", "pyyaml",
"requests",
], ],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [