From 064144134ea8ed56484d8e04cf64e6847d377511 Mon Sep 17 00:00:00 2001 From: Fredrick Amnehagen Date: Thu, 5 Feb 2026 19:17:54 +0100 Subject: [PATCH] refactor: move cloudflare domain list to dynamic state file --- README.md | 14 ++++++++--- infra_cli/cloudflare.py | 51 ++++++++++++++++++++++++++++++++++------- infra_cli/main.py | 20 ++++++++++++++++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6858b86..c9be3b5 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,18 @@ infra dns add-host "aa:bb:cc:dd:ee:ff" "10.32.70.100" "new-app" infra dns add-dns "api.loopaware.com" "10.32.70.100" ``` -### 4. Public Ingress (HAProxy) +### 4. Cloudflare DDNS +The list of domains to update is managed dynamically on the server. + ```bash -# Expose the service to the internet -infra ingress add "app.loopaware.com" "10.32.70.100" 80 +# Add a domain to the update list +infra cloudflare add-ddns "my-new-domain.com" + +# List all domains being updated +infra cloudflare list-ddns + +# Run the update (usually via cron) +infra cloudflare update-ddns ``` ## Advanced Workflows for AI Agents diff --git a/infra_cli/cloudflare.py b/infra_cli/cloudflare.py index 80d81c9..5067d7c 100644 --- a/infra_cli/cloudflare.py +++ b/infra_cli/cloudflare.py @@ -1,16 +1,51 @@ import requests -import sys +import json +import os class CloudflareManager: def __init__(self, config): self.token = config.get('cloudflare.token') - self.ddns_domains = config.get('cloudflare.ddns_domains', []) + # Path to store the dynamic list of domains + self.state_file = config.get('cloudflare.ddns_state_file', '/etc/haproxy/dynamic/ddns_domains.json') self.api_url = "https://api.cloudflare.com/client/v4" self.headers = { "Authorization": f"Bearer {self.token}", "Content-Type": "application/json" } + def _load_domains(self): + if not os.path.exists(self.state_file): + return [] + try: + with open(self.state_file, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + return [] + + def _save_domains(self, domains): + os.makedirs(os.path.dirname(self.state_file), exist_ok=True) + with open(self.state_file, 'w') as f: + json.dump(list(set(domains)), f, indent=4) + + def add_domain(self, domain): + domains = self._load_domains() + if domain not in domains: + domains.append(domain) + self._save_domains(domains) + return True + return False + + def remove_domain(self, domain): + domains = self._load_domains() + if domain in domains: + domains.remove(domain) + self._save_domains(domains) + return True + return False + + def list_domains(self): + return self._load_domains() + def get_external_ip(self): try: return requests.get("https://checkip.amazonaws.com").text.strip() @@ -30,8 +65,12 @@ class CloudflareManager: if not current_ip: return "Failed to determine current external IP." + domains = self._load_domains() + if not domains: + return "No domains configured for DDNS." + results = [] - for domain in self.ddns_domains: + for domain in domains: zone_id = self.get_zone_id(domain) if not zone_id: results.append(f"[{domain}] Zone not found.") @@ -42,7 +81,6 @@ class CloudflareManager: 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 @@ -67,7 +105,4 @@ class CloudflareManager: else: results.append(f"[{domain}] Update failed: {u_res.text}") - return "\n".join(results) - - def list_domains(self): - return self.ddns_domains + return "\n".join(results) \ No newline at end of file diff --git a/infra_cli/main.py b/infra_cli/main.py index c7d4f53..93a388e 100644 --- a/infra_cli/main.py +++ b/infra_cli/main.py @@ -31,6 +31,26 @@ def cf_list_ddns(config): for domain in mgr.list_domains(): click.echo(domain) +@cloudflare.command(name='add-ddns') +@click.argument('domain') +@click.pass_obj +def cf_add_ddns(config, domain): + mgr = CloudflareManager(config) + if mgr.add_domain(domain): + click.echo(f"Added {domain} to DDNS update list") + else: + click.echo(f"{domain} already in DDNS update list") + +@cloudflare.command(name='remove-ddns') +@click.argument('domain') +@click.pass_obj +def cf_remove_ddns(config, domain): + mgr = CloudflareManager(config) + if mgr.remove_domain(domain): + click.echo(f"Removed {domain} from DDNS update list") + else: + click.echo(f"{domain} not found in DDNS update list") + @cloudflare.command(name='update-ddns') @click.option('--force', is_flag=True, help='Force update even if IP matches') @click.pass_obj