feat(07-03): convert install_service.sh to Python with YAML config parsing

This commit is contained in:
2026-04-23 12:47:27 +03:00
parent 1621625bfb
commit 63f72c11ff

183
scripts/install_service.py Executable file
View File

@@ -0,0 +1,183 @@
#!/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()