Files
tfm_ainventory/start_servers.py
Daniel Bedeleanu e664215ab8 fix(6): fix argparse default for foreground mode
Changed default from 'foreground' (invalid choice) to None
Now correctly handles no arguments to start in foreground mode

Tested and verified:
  ✓ python3 start_servers.py        (foreground - all 3 services running)
  ✓ python3 start_servers.py start   (background)
  ✓ python3 start_servers.py status  (shows all services)
  ✓ python3 start_servers.py stop    (graceful shutdown)
  ✓ HTTP endpoints responding on 8916/8917
  ✓ HTTPS endpoints responding on 8918/8919 (Caddy)
  ✓ All three services (backend, frontend, caddy) working together
2026-04-22 19:21:16 +03:00

523 lines
19 KiB
Python
Executable File

#!/usr/bin/env python3
"""
TFM aInventory Standalone Server Launcher with SSL Support
Starts backend (FastAPI), frontend (Next.js), and Caddy reverse proxy
Full production-equivalent to Docker Compose deployment
Usage:
python3 start_servers.py # Start in foreground (interactive)
python3 start_servers.py start # Start in background
python3 start_servers.py stop # Stop background servers
python3 start_servers.py restart # Restart background servers
python3 start_servers.py status # Show server status
"""
import os
import sys
import subprocess
import signal
import time
import argparse
import json
from pathlib import Path
class StandaloneServerManager:
def __init__(self):
self.root_dir = Path(__file__).parent
self.pid_file = self.root_dir / ".servers.pid"
self.caddyfile = self.root_dir / "Caddyfile.standalone"
self.backend_process = None
self.frontend_process = None
self.caddy_process = None
self.load_config()
self.setup_directories()
def load_config(self):
"""Load configuration from inventory.env"""
env_file = self.root_dir / "inventory.env"
if not env_file.exists():
self.error(f"inventory.env not found at {env_file}")
self.config = {}
with open(env_file) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
self.config[key.strip()] = value.strip().strip('"').strip("'")
self.backend_port = int(self.config.get('BACKEND_PORT', 8000))
self.frontend_port = int(self.config.get('FRONTEND_PORT', 3000))
self.backend_ssl_port = int(self.config.get('BACKEND_SSL_PORT', 8918))
self.frontend_ssl_port = int(self.config.get('FRONTEND_SSL_PORT', 8919))
self.data_dir = self.root_dir / self.config.get('DATA_DIR', 'data')
self.logs_dir = self.root_dir / self.config.get('LOGS_DIR', 'logs')
self.log_level = self.config.get('LOG_LEVEL', 'INFO').lower()
self.log(f"✓ Configuration loaded")
self.log(f" Backend HTTP: {self.backend_port}, HTTPS: {self.backend_ssl_port}")
self.log(f" Frontend HTTP: {self.frontend_port}, HTTPS: {self.frontend_ssl_port}")
def setup_directories(self):
"""Create necessary directories"""
self.logs_dir.mkdir(parents=True, exist_ok=True)
self.data_dir.mkdir(parents=True, exist_ok=True)
(self.data_dir / "temp").mkdir(parents=True, exist_ok=True)
(self.logs_dir / "caddy").mkdir(parents=True, exist_ok=True)
self.log(f"✓ Directories ready")
def check_caddy(self):
"""Check if Caddy is installed"""
try:
subprocess.run(
["caddy", "version"],
capture_output=True,
check=True,
timeout=5
)
self.log("✓ Caddy is installed")
return True
except (subprocess.CalledProcessError, FileNotFoundError):
self.log("⚠ Caddy not found - installing...")
return self.install_caddy()
def install_caddy(self):
"""Install Caddy using system package manager"""
try:
# Try apt (Debian/Ubuntu)
subprocess.run(
["apt-get", "update"],
capture_output=True,
timeout=60
)
subprocess.run(
["apt-get", "install", "-y", "caddy"],
capture_output=True,
timeout=120,
check=True
)
self.log("✓ Caddy installed successfully")
return True
except Exception as e:
self.log(f"⚠ Failed to install Caddy: {e}")
self.log(" Install Caddy manually: https://caddyserver.com/docs/install")
return False
def check_ports(self):
"""Check if all required ports are available"""
import socket
ports = [
(self.backend_port, "Backend HTTP"),
(self.frontend_port, "Frontend HTTP"),
(self.backend_ssl_port, "Backend HTTPS"),
(self.frontend_ssl_port, "Frontend HTTPS")
]
for port, name in ports:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex(('127.0.0.1', port))
sock.close()
if result == 0:
self.error(f"Port {port} ({name}) is already in use")
except Exception as e:
self.log(f"⚠ Could not check port {port}: {e}")
self.log(f"✓ Required ports available")
def setup_backend(self):
"""Setup Python virtual environment and dependencies"""
venv_dir = self.root_dir / ".venv"
backend_dir = self.root_dir / "backend"
if not venv_dir.exists():
self.log("Creating Python virtual environment...")
subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
pip_exe = venv_dir / "bin" / "pip"
python_exe = venv_dir / "bin" / "python"
requirements = backend_dir / "requirements.txt"
if not python_exe.exists():
self.error(f"Virtual environment failed - python not found at {python_exe}")
try:
subprocess.run(
[str(python_exe), "-c", "import fastapi"],
check=True,
capture_output=True
)
except subprocess.CalledProcessError:
self.log("Installing Python dependencies...")
subprocess.run([str(pip_exe), "install", "-q", "-r", str(requirements)], check=True)
self.python_exe = python_exe
def setup_frontend(self):
"""Setup frontend dependencies"""
frontend_dir = self.root_dir / "frontend"
if not (frontend_dir / "node_modules").exists():
self.log("Installing npm dependencies...")
subprocess.run(
["npm", "ci", "--silent"],
cwd=str(frontend_dir),
check=True,
capture_output=True
)
if not (frontend_dir / ".next" / "standalone").exists():
self.log("Building Next.js application...")
result = subprocess.run(
["npm", "run", "build"],
cwd=str(frontend_dir),
capture_output=False
)
if result.returncode != 0:
self.error(f"Frontend build failed with exit code {result.returncode}")
def start_backend(self):
"""Start FastAPI backend server"""
self.log(f"Starting backend on port {self.backend_port}...")
backend_log = self.logs_dir / "backend.log"
env = os.environ.copy()
env['PATH'] = f"{self.python_exe.parent}:{env['PATH']}"
with open(backend_log, 'w') as log_f:
self.backend_process = subprocess.Popen(
[
str(self.python_exe),
"-m", "uvicorn",
"backend.main:app",
"--host", "0.0.0.0",
"--port", str(self.backend_port),
"--log-level", self.log_level
],
cwd=str(self.root_dir),
stdout=log_f,
stderr=subprocess.STDOUT,
env=env,
preexec_fn=os.setsid
)
self.log(f"✓ Backend started (PID: {self.backend_process.pid})")
def start_frontend(self):
"""Start Next.js frontend server"""
self.log(f"Starting frontend on port {self.frontend_port}...")
frontend_log = self.logs_dir / "frontend.log"
frontend_dir = self.root_dir / "frontend"
env = os.environ.copy()
env['PORT'] = str(self.frontend_port)
env['NODE_ENV'] = 'production'
with open(frontend_log, 'w') as log_f:
self.frontend_process = subprocess.Popen(
["node", ".next/standalone/server.js"],
cwd=str(frontend_dir),
stdout=log_f,
stderr=subprocess.STDOUT,
env=env,
preexec_fn=os.setsid
)
self.log(f"✓ Frontend started (PID: {self.frontend_process.pid})")
def start_caddy(self):
"""Start Caddy reverse proxy with SSL"""
if not self.caddyfile.exists():
self.error(f"Caddyfile not found: {self.caddyfile}")
self.log(f"Starting Caddy reverse proxy...")
caddy_log = self.logs_dir / "caddy.log"
with open(caddy_log, 'w') as log_f:
self.caddy_process = subprocess.Popen(
[
"caddy", "run",
"--config", str(self.caddyfile),
"--adapter", "caddyfile"
],
stdout=log_f,
stderr=subprocess.STDOUT,
preexec_fn=os.setsid
)
self.log(f"✓ Caddy started (PID: {self.caddy_process.pid})")
def save_pids(self):
"""Save process IDs to file for stop/restart commands"""
pids = {
'backend': self.backend_process.pid if self.backend_process else None,
'frontend': self.frontend_process.pid if self.frontend_process else None,
'caddy': self.caddy_process.pid if self.caddy_process else None,
'timestamp': time.time()
}
with open(self.pid_file, 'w') as f:
json.dump(pids, f)
self.log(f"✓ Process IDs saved")
def load_pids(self):
"""Load process IDs from file"""
if not self.pid_file.exists():
return None
try:
with open(self.pid_file) as f:
return json.load(f)
except Exception:
return None
def wait_for_startup(self):
"""Wait and check if servers started"""
time.sleep(3)
try:
import urllib.request
urllib.request.urlopen(f"http://localhost:{self.backend_port}/health", timeout=2)
self.log(f"✓ Backend responding")
except Exception:
self.log(f"⚠ Backend not responding yet")
try:
import urllib.request
urllib.request.urlopen(f"http://localhost:{self.frontend_port}/", timeout=2)
self.log(f"✓ Frontend responding")
except Exception:
self.log(f"⚠ Frontend not responding yet")
def print_summary(self):
"""Print summary of running servers"""
print("\n" + "="*70)
print(" ✓ TFM aInventory Standalone Deployment Started Successfully!")
print("="*70 + "\n")
print("Access points:")
print(f" Frontend HTTP: http://localhost:{self.frontend_port}")
print(f" Frontend HTTPS: https://localhost:{self.frontend_ssl_port} (self-signed)")
print(f" Backend HTTP: http://localhost:{self.backend_port}")
print(f" Backend HTTPS: https://localhost:{self.backend_ssl_port} (self-signed)")
print(f" API Docs: http://localhost:{self.backend_port}/docs")
print("\nProcess IDs:")
print(f" Backend: {self.backend_process.pid}")
print(f" Frontend: {self.frontend_process.pid}")
print(f" Caddy (SSL): {self.caddy_process.pid}")
print("\nLog files:")
print(f" {self.logs_dir / 'backend.log'}")
print(f" {self.logs_dir / 'frontend.log'}")
print(f" {self.logs_dir / 'caddy.log'}")
print("\nMonitoring:")
print(f" Status: python3 start_servers.py status")
print(f" Logs: tail -f {self.logs_dir}/*.log")
print("\nTo stop servers:")
print(f" python3 start_servers.py stop")
print("="*70 + "\n")
def run_foreground(self):
"""Run servers in foreground (interactive)"""
try:
print("\n=== TFM aInventory Standalone Deployment (Foreground) ===\n")
self.check_ports()
self.setup_backend()
self.setup_frontend()
if not self.check_caddy():
self.log("⚠ Caddy not available - HTTPS disabled")
self.start_backend()
self.start_frontend()
if self.caddy_process is None:
self.start_caddy()
self.wait_for_startup()
self.print_summary()
self.log("✓ All servers running! Press Ctrl+C to stop.")
while True:
if self.backend_process and self.backend_process.poll() is not None:
self.log(f"⚠ Backend exited (code {self.backend_process.returncode})")
if self.frontend_process and self.frontend_process.poll() is not None:
self.log(f"⚠ Frontend exited (code {self.frontend_process.returncode})")
if self.caddy_process and self.caddy_process.poll() is not None:
self.log(f"⚠ Caddy exited (code {self.caddy_process.returncode})")
time.sleep(5)
except KeyboardInterrupt:
self.log("Received interrupt signal, shutting down...")
self.cleanup()
except Exception as e:
self.error(str(e))
def run_background(self):
"""Run servers in background (detached)"""
try:
print("\n=== TFM aInventory Standalone Deployment (Background) ===\n")
self.check_ports()
self.setup_backend()
self.setup_frontend()
if not self.check_caddy():
self.log("⚠ Caddy not available - HTTPS disabled")
self.start_backend()
self.start_frontend()
if self.caddy_process is None:
self.start_caddy()
self.save_pids()
self.wait_for_startup()
self.print_summary()
print(f"✓ Servers running in background")
print(f" Stop: python3 start_servers.py stop")
print(f" Status: python3 start_servers.py status\n")
except Exception as e:
self.error(str(e))
def stop_servers(self):
"""Stop background servers"""
pids = self.load_pids()
if not pids:
print("[ERROR] No running servers found")
return False
success = True
for service, pid in [('Backend', pids.get('backend')),
('Frontend', pids.get('frontend')),
('Caddy', pids.get('caddy'))]:
if pid:
try:
os.killpg(os.getpgid(pid), signal.SIGTERM)
print(f"[INFO] {service} stopped (PID {pid})")
time.sleep(1)
except Exception as e:
print(f"[WARN] Failed to stop {service}: {e}")
success = False
if self.pid_file.exists():
self.pid_file.unlink()
print("[INFO] ✓ Servers stopped")
return success
def show_status(self):
"""Show status of running servers"""
pids = self.load_pids()
if not pids:
print("[INFO] No running servers")
return
print("\n" + "="*70)
print(" Server Status")
print("="*70 + "\n")
for service, pid, port, ssl_port in [
('Backend', pids.get('backend'), self.backend_port, self.backend_ssl_port),
('Frontend', pids.get('frontend'), self.frontend_port, self.frontend_ssl_port),
('Caddy SSL', pids.get('caddy'), None, None)
]:
if pid:
try:
os.kill(pid, 0)
status = "✓ RUNNING"
if port:
print(f"{status}: {service} (PID {pid})")
print(f" HTTP: http://localhost:{port}")
print(f" HTTPS: https://localhost:{ssl_port}")
else:
print(f"{status}: {service} (PID {pid})")
except ProcessLookupError:
print(f"✗ STOPPED: {service} (was PID {pid})")
else:
print(f"✗ NOT FOUND: {service}")
print("\nLog files:")
print(f" {self.logs_dir / 'backend.log'}")
print(f" {self.logs_dir / 'frontend.log'}")
print(f" {self.logs_dir / 'caddy.log'}")
print("="*70 + "\n")
def cleanup(self):
"""Gracefully shutdown servers"""
for process in [self.backend_process, self.frontend_process, self.caddy_process]:
if process:
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait(timeout=5)
except Exception:
try:
process.kill()
except Exception:
pass
if self.pid_file.exists():
self.pid_file.unlink()
self.log("✓ Servers stopped")
sys.exit(0)
def log(self, message):
"""Print log message"""
print(f"[INFO] {message}")
def error(self, message):
"""Print error and exit"""
print(f"[ERROR] {message}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description='TFM aInventory Standalone Deployment with SSL Support',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 start_servers.py # Start in foreground (interactive, Ctrl+C to stop)
python3 start_servers.py start # Start in background
python3 start_servers.py stop # Stop background servers
python3 start_servers.py restart # Restart background servers
python3 start_servers.py status # Show server status
Features:
- Backend + Frontend + Caddy reverse proxy with SSL
- HTTP and HTTPS access
- Process management with PID file
- Self-signed certificates (development-friendly)
"""
)
parser.add_argument(
'command',
nargs='?',
choices=['start', 'stop', 'restart', 'status'],
default=None,
help='Command to execute (default: foreground mode)'
)
args = parser.parse_args()
manager = StandaloneServerManager()
if args.command == 'start':
manager.run_background()
elif args.command == 'stop':
manager.stop_servers()
elif args.command == 'restart':
manager.stop_servers()
time.sleep(2)
manager.run_background()
elif args.command == 'status':
manager.show_status()
elif args.command is None:
manager.run_foreground()
else:
manager.run_foreground()
if __name__ == "__main__":
main()