Flask app progress
This commit is contained in:
parent
c4fe022d42
commit
b0994069ad
38 changed files with 6631 additions and 220 deletions
117
router/ddns.py
117
router/ddns.py
|
|
@ -18,9 +18,10 @@ Log is cleared when it exceeds general.log_max_kb from config.
|
|||
Usage:
|
||||
sudo python3 ddns.py --start Run update and install systemd timer
|
||||
sudo python3 ddns.py --disable Stop updates and remove systemd timer
|
||||
sudo python3 ddns.py --apply Run update once (used by timer)
|
||||
sudo python3 ddns.py --force Force update regardless of cached IP
|
||||
sudo python3 ddns.py --status Show timer/service status
|
||||
python3 ddns.py --apply Run update once (used by timer)
|
||||
python3 ddns.py --force Force update regardless of cached IP
|
||||
python3 ddns.py --status Show timer/service status
|
||||
python3 ddns.py --getip Print current public IP and exit
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -44,13 +45,13 @@ TIMER_FILE = Path(f"/etc/systemd/system/{TIMER_NAME}.timer")
|
|||
# log is assigned in setup_logging() after config is loaded
|
||||
log = None
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Load config
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def load_config():
|
||||
if not CONFIG_FILE.exists():
|
||||
print(f"ERROR: Config file not found: {CONFIG_FILE}")
|
||||
print(f"ERROR: Config file not found: {CONFIG_FILE}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
with open(CONFIG_FILE) as f:
|
||||
data = json.load(f)
|
||||
|
|
@ -59,47 +60,47 @@ def load_config():
|
|||
required_general = {"log_max_kb", "log_errors_only", "ip_check_services"}
|
||||
missing = required_general - set(data.get("general", {}).keys())
|
||||
if missing:
|
||||
print(f"ERROR: Missing keys in general block: {missing}")
|
||||
print(f"ERROR: Missing keys in general block: {missing}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not data["general"]["ip_check_services"]:
|
||||
print("ERROR: ip_check_services list is empty.")
|
||||
print("ERROR: ip_check_services list is empty.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Validate providers block
|
||||
if not data.get("providers"):
|
||||
print("ERROR: No providers defined in config.")
|
||||
print("ERROR: No providers defined in config.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
for p in data["providers"]:
|
||||
base_required = {"description", "provider", "enabled"}
|
||||
missing = base_required - set(p.keys())
|
||||
if missing:
|
||||
print(f"ERROR: Provider '{p.get('description', '?')}' missing keys: {missing}")
|
||||
print(f"ERROR: Provider '{p.get('description', '?')}' missing keys: {missing}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
ptype = p.get("provider", "").lower()
|
||||
if ptype == "noip":
|
||||
extra = {"username", "password", "hostnames"}
|
||||
elif ptype == "duckdns":
|
||||
extra = {"token", "subdomains"}
|
||||
extra = {"api_token", "hostnames"}
|
||||
elif ptype == "cloudflare":
|
||||
extra = {"api_token", "hostnames"}
|
||||
else:
|
||||
print(f"ERROR: Provider '{p.get('description', '?')}' has unknown provider type: '{ptype}'")
|
||||
print(f"ERROR: Provider '{p.get('description', '?')}' has unknown provider type: '{ptype}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
missing = extra - set(p.keys())
|
||||
if missing:
|
||||
print(f"ERROR: Provider '{p.get('description', '?')}' missing keys for {ptype}: {missing}")
|
||||
print(f"ERROR: Provider '{p.get('description', '?')}' missing keys for {ptype}: {missing}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return data
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def chown_to_script_dir_owner(path):
|
||||
"""Chown a file to the owner of the script directory.
|
||||
This works correctly whether invoked via sudo, directly as root (e.g. systemd timer),
|
||||
or as a normal user — the script directory owner is always the right target.
|
||||
or as a normal user - the script directory owner is always the right target.
|
||||
"""
|
||||
try:
|
||||
stat = SCRIPT_DIR.stat()
|
||||
|
|
@ -107,9 +108,9 @@ def chown_to_script_dir_owner(path):
|
|||
except OSError:
|
||||
pass # non-fatal
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Logging
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def setup_logging(max_kb, errors_only):
|
||||
"""Clear log if oversized, then initialise logger. Must be called before log is used."""
|
||||
|
|
@ -138,9 +139,9 @@ def setup_logging(max_kb, errors_only):
|
|||
)
|
||||
log = logging.getLogger("ddns")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Per-provider IP cache
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def cache_file_for(description):
|
||||
"""Return the cache file path for a given provider description."""
|
||||
|
|
@ -158,9 +159,9 @@ def save_cached_ip(description, ip):
|
|||
f.write_text(ip)
|
||||
chown_to_script_dir_owner(f)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Service rotation
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def get_next_service_index(total):
|
||||
"""Read last used index, increment, wrap around, return next index."""
|
||||
|
|
@ -177,8 +178,9 @@ def save_service_index(index):
|
|||
CACHE_SERVICE_FILE.write_text(str(index))
|
||||
chown_to_script_dir_owner(CACHE_SERVICE_FILE)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Public IP detection
|
||||
# ===================================================================
|
||||
|
||||
def extract_ip(body):
|
||||
"""
|
||||
|
|
@ -225,7 +227,7 @@ def _get_ip_via_cf_dns(spec):
|
|||
return None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def get_public_ip(services):
|
||||
"""
|
||||
|
|
@ -258,9 +260,9 @@ def get_public_ip(services):
|
|||
log.error("Could not determine public IP from any configured service.")
|
||||
sys.exit(1)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# No-IP update
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def update_noip(provider, ip):
|
||||
"""
|
||||
|
|
@ -325,9 +327,9 @@ def interpret_noip_response(response, hostnames, ip):
|
|||
return False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# DuckDNS update
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def update_duckdns(provider, ip):
|
||||
"""
|
||||
|
|
@ -338,8 +340,8 @@ def update_duckdns(provider, ip):
|
|||
as a comma-separated list.
|
||||
Returns True on success, False on failure.
|
||||
"""
|
||||
token = provider["token"]
|
||||
subdomains = ",".join(provider["subdomains"])
|
||||
token = provider["api_token"]
|
||||
subdomains = ",".join(h.replace(".duckdns.org", "") for h in provider["hostnames"])
|
||||
description = provider["description"]
|
||||
|
||||
url = f"https://www.duckdns.org/update?domains={subdomains}&token={token}&ip={ip}"
|
||||
|
|
@ -358,9 +360,9 @@ def update_duckdns(provider, ip):
|
|||
log.error(f"Network error contacting DuckDNS: {e}")
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Cloudflare DNS update
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def _cf_api_get(url, headers):
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
|
|
@ -429,9 +431,9 @@ def update_cloudflare(provider, ip):
|
|||
success = False
|
||||
return success
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Process a single provider block
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def process_provider(provider, current_ip, force=False):
|
||||
description = provider["description"]
|
||||
|
|
@ -471,9 +473,9 @@ def process_provider(provider, current_ip, force=False):
|
|||
save_cached_ip(description, current_ip)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Timer management
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def parse_interval(interval_str):
|
||||
"""
|
||||
|
|
@ -557,16 +559,22 @@ def remove_timer():
|
|||
else:
|
||||
print("No timer found, nothing to remove.")
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
# Main
|
||||
# ------------------------------------------------------------------------------
|
||||
# ===================================================================
|
||||
|
||||
def run_update(cfg, force=False):
|
||||
def run_update(cfg, force=False, getip_only=False):
|
||||
"""Perform a single DDNS update pass. Called by both timer and --start.
|
||||
If force=True, bypasses the cached IP check and always updates."""
|
||||
If force=True, bypasses the cached IP check and always updates.
|
||||
If getip_only=True, prints the detected public IP and returns without updating providers."""
|
||||
general = cfg["general"]
|
||||
current_ip = get_public_ip(general["ip_check_services"])
|
||||
enabled = [p for p in cfg["providers"] if p.get("enabled") is True]
|
||||
|
||||
if getip_only:
|
||||
print(current_ip)
|
||||
return
|
||||
|
||||
enabled = [p for p in cfg["providers"] if p.get("enabled") is True]
|
||||
|
||||
if not enabled:
|
||||
log.error("No enabled providers found in config.")
|
||||
|
|
@ -594,20 +602,22 @@ def main():
|
|||
"examples:\n"
|
||||
" sudo python3 ddns.py --start Run update and install systemd timer\n"
|
||||
" sudo python3 ddns.py --disable Stop updates and remove systemd timer\n"
|
||||
" sudo python3 ddns.py --apply Run update once (used by timer)\n"
|
||||
" sudo python3 ddns.py --force Force update regardless of cached IP\n"
|
||||
" sudo python3 ddns.py --status Show timer/service status\n"
|
||||
" python3 ddns.py --apply Run update once (used by timer)\n"
|
||||
" python3 ddns.py --force Force update regardless of cached IP\n"
|
||||
" python3 ddns.py --status Show timer/service status\n"
|
||||
" python3 ddns.py --getip Print current public IP and exit\n"
|
||||
)
|
||||
)
|
||||
parser.add_argument("--start", action="store_true", help="Run update and install systemd timer")
|
||||
parser.add_argument("--start", action="store_true", help="Run update and install systemd timer")
|
||||
parser.add_argument("--disable", action="store_true", help="Stop updates and remove systemd timer")
|
||||
parser.add_argument("--apply", action="store_true", help="Run update once (used by timer)")
|
||||
parser.add_argument("--force", action="store_true", help="Force update regardless of cached IP")
|
||||
parser.add_argument("--status", action="store_true", help="Show timer/service status")
|
||||
parser.add_argument("--apply", action="store_true", help="Run update once (used by timer)")
|
||||
parser.add_argument("--force", action="store_true", help="Force update regardless of cached IP")
|
||||
parser.add_argument("--status", action="store_true", help="Show timer/service status")
|
||||
parser.add_argument("--getip", action="store_true", help="Print current public IP and exit")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not any([args.start, args.disable, args.apply, args.force, args.status]):
|
||||
if not any([args.start, args.disable, args.apply, args.force, args.status, args.getip]):
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
|
|
@ -615,6 +625,15 @@ def main():
|
|||
show_status()
|
||||
return
|
||||
|
||||
if args.getip:
|
||||
global log
|
||||
log = logging.getLogger("ddns_quiet")
|
||||
log.addHandler(logging.NullHandler())
|
||||
log.propagate = False
|
||||
cfg = load_config()
|
||||
run_update(cfg, getip_only=True)
|
||||
return
|
||||
|
||||
cfg = load_config()
|
||||
general = cfg["general"]
|
||||
setup_logging(general["log_max_kb"], general["log_errors_only"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue