#!/usr/bin/env python3 """ TFM aInventory - Systemd Service Installer (v1.12.0) Converted from install_service.sh to Python per Decision D-05. """ import os import sys import yaml import argparse import subprocess import logging import pwd import grp from typing import Dict, Any # Color codes class Colors: RED = '\033[0;31m' GREEN = '\033[0;32m' YELLOW = '\033[1;33m' BLUE = '\033[0;34m' NC = '\033[0m' # Setup logging logging.basicConfig(level=logging.INFO, format=f"{Colors.BLUE}[INFO]{Colors.NC} %(message)s") logger = logging.getLogger("install_service") SERVICE_NAME = "ainventory.service" SERVICE_PATH = f"/etc/systemd/system/{SERVICE_NAME}" # Inline template if file missing DEFAULT_TEMPLATE = """[Unit] Description=TFM aInventory Service After=network.target [Service] Type=simple User={SERVICE_USER} Group={SERVICE_USER} WorkingDirectory={PROJECT_DIR} Environment=PATH={PROJECT_DIR}/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin ExecStart={PYTHON_PATH} {PROJECT_DIR}/scripts/run_standalone.py --backend-only Restart=on-failure RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target """ def load_yaml(file_path: str) -> Dict[str, Any]: if not os.path.exists(file_path): return {} try: with open(file_path, 'r') as f: return yaml.safe_load(f) or {} except Exception as e: logger.error(f"Failed to parse {file_path}: {e}") return {} def check_user_exists(username: str) -> bool: try: pwd.getpwnam(username) return True except KeyError: return False def main(): if os.getuid() != 0: logger.error("This script must be run as root (use sudo)") sys.exit(1) parser = argparse.ArgumentParser(description="TFM aInventory Systemd Service Installer") parser.add_argument("--user", default="www-data", help="Service user (default: www-data)") parser.add_argument("--force", action="store_true", help="Overwrite existing service file") args = parser.parse_args() project_dir = os.getcwd() python_path = sys.executable # Load configs backend_cfg = load_yaml("config/backend.yaml") network_cfg = load_yaml("config/network.yaml") app_cfg = backend_cfg.get("application", {}) ports_cfg = network_cfg.get("ports", {}) backend_port = ports_cfg.get("backend_port", 8916) data_dir = os.path.abspath(app_cfg.get("data_dir", "./data")) logs_dir = os.path.abspath(app_cfg.get("logs_dir", "./logs")) logger.info(f"Installing {SERVICE_NAME}...") if not check_user_exists(args.user): logger.warning(f"User '{args.user}' does not exist. Suggestion: sudo useradd -r -s /bin/false {args.user}") # We'll continue but warn, as user might want to create it later (though service won't start) if os.path.exists(SERVICE_PATH) and not args.force: logger.error(f"Service file {SERVICE_PATH} already exists. Use --force to overwrite.") sys.exit(1) # Read template if exists template_content = DEFAULT_TEMPLATE if os.path.exists("inventory.service.template"): try: with open("inventory.service.template", "r") as f: content = f.read() if content.strip(): template_content = content # If using old template with __WORKING_DIR__, replace it template_content = template_content.replace("__WORKING_DIR__", "{PROJECT_DIR}") except Exception as e: logger.warning(f"Failed to read inventory.service.template: {e}. Using default.") # Format template # Note: We need to handle keys that might not be in the template but are in our variables # The plan mentions {PROJECT_DIR}, {SERVICE_USER}, {BACKEND_PORT}, {DATA_DIR}, {LOGS_DIR} try: # We'll use a safer way to replace placeholders to avoid KeyError if template doesn't have all of them service_content = template_content.format( PROJECT_DIR=project_dir, SERVICE_USER=args.user, BACKEND_PORT=backend_port, DATA_DIR=data_dir, LOGS_DIR=logs_dir, PYTHON_PATH=python_path ) except KeyError as e: # Fallback if template has unknown placeholders logger.warning(f"Template contains unknown placeholder: {e}. Attempting simple replacement.") service_content = template_content.replace("{PROJECT_DIR}", project_dir)\ .replace("{SERVICE_USER}", args.user)\ .replace("{BACKEND_PORT}", str(backend_port))\ .replace("{DATA_DIR}", data_dir)\ .replace("{LOGS_DIR}", logs_dir)\ .replace("{PYTHON_PATH}", python_path) # Write service file try: with open(SERVICE_PATH, "w") as f: f.write(service_content) os.chmod(SERVICE_PATH, 0o644) logger.info(f"✓ Service file created at {SERVICE_PATH}") except Exception as e: logger.error(f"Failed to write service file: {e}") sys.exit(1) # Ensure data and logs directories exist and are owned by the service user for d in [data_dir, logs_dir]: os.makedirs(d, exist_ok=True) try: uid = pwd.getpwnam(args.user).pw_uid gid = grp.getgrnam(args.user).gr_gid os.chown(d, uid, gid) # Also chown contents if any for root, dirs, files in os.walk(d): for momo in dirs: os.chown(os.path.join(root, momo), uid, gid) for momo in files: os.chown(os.path.join(root, momo), uid, gid) except Exception as e: logger.warning(f"Could not set permissions for {d}: {e}") # Reload systemd try: subprocess.run(["systemctl", "daemon-reload"], check=True) subprocess.run(["systemctl", "enable", SERVICE_NAME], check=True) logger.info(f"✓ {SERVICE_NAME} enabled") except subprocess.CalledProcessError as e: logger.error(f"Failed to enable service: {e}") sys.exit(1) print(f"\n{Colors.GREEN}🚀 TFM aInventory service installed successfully!{Colors.NC}") print(f"Service user: {args.user}") print(f"Project dir: {project_dir}") print("\nNext steps:") print(f" 👉 sudo systemctl start {SERVICE_NAME}") print(f" 👉 sudo systemctl status {SERVICE_NAME}") print(f" 👉 journalctl -u {SERVICE_NAME} -f") if __name__ == "__main__": main()