#!/usr/bin/env python3 """Generic job runner for scheduled tasks in Docker containers. Runs a Flask CLI command on a configurable interval with graceful shutdown support. Environment Variables: JOB_NAME: Name of the job to run (zerotier_reconciliation, mfa_compliance) JOB_INTERVAL_SECONDS: Seconds between job runs (default: 300) Usage: docker run -e JOB_NAME=zerotier_reconciliation -e JOB_INTERVAL_SECONDS=120 app """ import os import signal import subprocess import sys import time import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) logger = logging.getLogger(__name__) JOB_COMMANDS = { "zerotier_reconciliation": "python manage.py run_zerotier_reconciliation", "mfa_compliance": "python manage.py run_mfa_compliance_job", "cleanup_sessions": "python manage.py cleanup_sessions", } shutdown_requested = False def signal_handler(signum, frame): global shutdown_requested logger.info(f"Received signal {signum}, initiating graceful shutdown...") shutdown_requested = True def run_job(job_name: str) -> bool: command = JOB_COMMANDS.get(job_name) if not command: logger.error(f"Unknown job: {job_name}. Valid jobs: {list(JOB_COMMANDS.keys())}") return False logger.info(f"Running job: {job_name}") start_time = time.monotonic() try: result = subprocess.run( command, shell=True, cwd="/app", capture_output=False, ) elapsed = time.monotonic() - start_time logger.info(f"Job {job_name} completed in {elapsed:.2f}s with exit code {result.returncode}") return result.returncode == 0 except Exception as e: elapsed = time.monotonic() - start_time logger.error(f"Job {job_name} failed after {elapsed:.2f}s: {e}") return False def main(): job_name = os.getenv("JOB_NAME") interval = int(os.getenv("JOB_INTERVAL_SECONDS", "300")) if not job_name: logger.error("JOB_NAME environment variable is required") sys.exit(1) if job_name not in JOB_COMMANDS: logger.error(f"Unknown JOB_NAME: {job_name}. Valid: {list(JOB_COMMANDS.keys())}") sys.exit(1) if interval < 10: logger.error(f"JOB_INTERVAL_SECONDS must be at least 10 seconds, got {interval}") sys.exit(1) signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) logger.info(f"Job runner started: {job_name}, interval={interval}s") logger.info(f"Valid jobs: {list(JOB_COMMANDS.keys())}") while not shutdown_requested: run_job(job_name) if shutdown_requested: break logger.info(f"Sleeping for {interval}s until next run...") sleep_start = time.monotonic() while time.monotonic() - sleep_start < interval and not shutdown_requested: time.sleep(1) logger.info("Job runner stopped") if __name__ == "__main__": main()