185 lines
5.9 KiB
Python
185 lines
5.9 KiB
Python
"""
|
||
Wire auto-publisher sites to Cloudflare home tunnel.
|
||
Adds ingress rules to the existing home tunnel for each site subdomain.
|
||
"""
|
||
import json
|
||
import subprocess
|
||
import time
|
||
|
||
# Home tunnel ID (the existing tunnel that routes ~38 services)
|
||
HOME_TUNNEL_ID = "d2871458-1737-4844-b114-9a44b9d71e25"
|
||
ACCT_ID = "895479ab3540daa6d61ae702a5164475"
|
||
ZONE_ID = "93634d23ff8138fbd795763b681158ac"
|
||
DOMAIN = "thetempleofdoom.com"
|
||
|
||
# Site IPs
|
||
SITES = {
|
||
"ai": "10.30.20.240",
|
||
"tech": "10.30.20.241",
|
||
"science": "10.30.20.242",
|
||
"crypto": "10.30.20.243",
|
||
"linux": "10.30.20.244",
|
||
"gaming": "10.30.20.246",
|
||
"diy": "10.30.20.247",
|
||
"guides": "10.30.20.248",
|
||
}
|
||
|
||
|
||
def get_home_tunnel_config():
|
||
"""Get current home tunnel ingress configuration."""
|
||
result = subprocess.run(
|
||
["cloudflared", "tunnel", "info", "--output", "json", HOME_TUNNEL_ID],
|
||
capture_output=True, text=True, timeout=10
|
||
)
|
||
if result.returncode == 0 and result.stdout.strip():
|
||
return json.loads(result.stdout)
|
||
return None
|
||
|
||
|
||
def add_ingress_rules():
|
||
"""Add ingress rules for all 8 publisher sites to the home tunnel."""
|
||
print("Adding Cloudflare tunnel ingress rules...")
|
||
|
||
for name, ip in SITES.items():
|
||
hostname = f"{name}.{DOMAIN}"
|
||
|
||
# Check if DNS record already exists
|
||
print(f"\n {hostname}:")
|
||
|
||
# Use cloudflared CLI to add route
|
||
cmd = [
|
||
"cloudflared", "tunnel", "route", "dns",
|
||
"--overwrite-dns",
|
||
HOME_TUNNEL_ID, hostname
|
||
]
|
||
|
||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||
if result.returncode == 0:
|
||
print(f" ✅ DNS route created: {hostname} → tunnel")
|
||
else:
|
||
error = result.stderr.strip()
|
||
if "already exists" in error.lower() or "conflict" in error.lower():
|
||
print(f" ⚠️ DNS already exists: {hostname}")
|
||
else:
|
||
print(f" ❌ Failed: {error[:200]}")
|
||
# Fallback: manual DNS via API
|
||
_add_dns_manual(name, hostname)
|
||
|
||
# Add local ingress mapping using config file approach
|
||
_add_config_ingress(name, hostname, ip)
|
||
|
||
|
||
def _add_dns_manual(name, hostname):
|
||
"""Manual DNS CNAME creation via API."""
|
||
# This requires a working CF token
|
||
CF_TOKEN = "cfat_M62Ke6eLXPZnb26qW4FCRL4T4h8l4KrRVLFIRWad9455c3e0"
|
||
|
||
result = subprocess.run([
|
||
"curl", "-s", "-X", "POST",
|
||
f"https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/dns_records",
|
||
"-H", f"Authorization: Bearer {CF_TOKEN}",
|
||
"-H", "Content-Type: application/json",
|
||
"-d", json.dumps({
|
||
"type": "CNAME",
|
||
"name": name,
|
||
"content": f"{HOME_TUNNEL_ID}.cfargotunnel.com",
|
||
"proxied": True,
|
||
"ttl": 1,
|
||
}),
|
||
], capture_output=True, text=True, timeout=15)
|
||
|
||
try:
|
||
resp = json.loads(result.stdout)
|
||
if resp.get("success"):
|
||
print(f" ✅ Manual DNS created: {hostname}")
|
||
else:
|
||
errors = resp.get("errors", [])
|
||
for e in errors:
|
||
if "already exists" in str(e).lower() or e.get("code") == 81053:
|
||
print(f" ⚠️ DNS already exists: {hostname}")
|
||
return
|
||
print(f" ❌ Manual DNS failed: {errors}")
|
||
except Exception:
|
||
print(f" ❌ API error: {result.stdout[:200]}")
|
||
|
||
|
||
def _add_config_ingress(name, hostname, ip):
|
||
"""Add ingress rule to cloudflared config file."""
|
||
config_path = "/usr/local/etc/cloudflared/config.yml"
|
||
|
||
# Check if cloudflared is running locally
|
||
result = subprocess.run(
|
||
["pgrep", "-f", "cloudflared"], capture_output=True, text=True
|
||
)
|
||
|
||
if result.returncode != 0:
|
||
print(f" ℹ️ cloudflared not running locally — tunnel is on Proxmox CT680")
|
||
print(f" ℹ️ Add manually: cloudflared tunnel route dns {HOME_TUNNEL_ID} {hostname}")
|
||
return
|
||
|
||
# Read existing config
|
||
try:
|
||
with open(config_path) as f:
|
||
config = f.read()
|
||
except FileNotFoundError:
|
||
print(f" ℹ️ No local cloudflared config — tunnel managed elsewhere")
|
||
return
|
||
|
||
# Check if entry exists
|
||
if f"hostname: {hostname}" in config:
|
||
print(f" ⚠️ Ingress already in config for {hostname}")
|
||
return
|
||
|
||
# Add ingress rule before the catch-all
|
||
new_rule = f"""
|
||
- hostname: {hostname}
|
||
service: http://{ip}:80"""
|
||
|
||
if "service: http_status:404" in config:
|
||
config = config.replace(
|
||
"service: http_status:404",
|
||
f"{new_rule}\n - service: http_status:404"
|
||
)
|
||
|
||
with open(config_path, "w") as f:
|
||
f.write(config)
|
||
|
||
print(f" ✅ Ingress added locally for {hostname}")
|
||
|
||
# Restart cloudflared
|
||
subprocess.run(["brew", "services", "restart", "cloudflared"],
|
||
capture_output=True, timeout=10)
|
||
print(f" ✅ cloudflared restarted")
|
||
|
||
|
||
def verify_all():
|
||
"""Verify all sites are accessible via Cloudflare tunnel."""
|
||
print("\n\nVerifying all sites...")
|
||
time.sleep(5)
|
||
|
||
for name in SITES:
|
||
url = f"https://{name}.{DOMAIN}"
|
||
result = subprocess.run(
|
||
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", url],
|
||
capture_output=True, text=True, timeout=10
|
||
)
|
||
code = result.stdout.strip()
|
||
status = "✅" if code in ("200", "301", "302") else "❌"
|
||
print(f" {status} {url} → HTTP {code}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
print("=" * 60)
|
||
print("CLOUDFLARE TUNNEL SETUP — Auto Publisher Sites")
|
||
print("=" * 60)
|
||
print(f"\nHome Tunnel: {HOME_TUNNEL_ID}")
|
||
print(f"Domain: {DOMAIN}")
|
||
print()
|
||
|
||
add_ingress_rules()
|
||
verify_all()
|
||
|
||
print(f"\n{'='*60}")
|
||
print("TUNNEL SETUP COMPLETE")
|
||
print(f"{'='*60}")
|