platform-tooling/scripts/orchestration/systemd-generator.ts
Quinn Ftw 85621b287e chore: snapshot before monorepo consolidation
Capture current working state before converting platform-tooling
into a submodule of the lilith-platform monorepo.
2026-01-29 07:04:39 -08:00

172 lines
5.3 KiB
JavaScript

#!/usr/bin/env node
/**
* systemd Service File Generator
* Generates systemd unit files for all production services
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { ProductionServiceConfig, getAllProductionConfigs } from './prod-services';
import { PATHS } from '../../configs/paths';
const OUTPUT_DIR = PATHS.systemdGenerated;
/**
* Generate systemd unit file content from config
*/
function generateUnitFile(config: ProductionServiceConfig): string {
const {
systemdUnit,
serviceType,
workingDir,
execStart,
port,
user,
group,
dependencies,
healthCheck,
gpu,
} = config;
// Build After= dependencies
const afterDeps = dependencies.join(' ');
// Build environment variables
const envVars: string[] = [];
if (port) {
envVars.push(`PORT=${port}`);
}
if (gpu) {
// Load port from @model-boss/infrastructure/ports.yaml
const { getModelBossPort } = await import('./external-config-loader.js');
const modelBossPort = getModelBossPort();
envVars.push(`MODEL_BOSS_URL=http://localhost:${modelBossPort}`);
}
// Build Environment= lines
const envLines = envVars.map(env => `Environment="${env}"`).join('\n');
// Build Requires= and After= based on service type
const requiresDeps = dependencies.filter(dep => dep !== 'network.target').join(' ');
return `# ${systemdUnit}
# Generated by systemd-generator.ts - DO NOT EDIT MANUALLY
# Service: ${config.serviceId}
# Type: ${serviceType}
[Unit]
Description=Lilith Platform - ${config.serviceId}
Documentation=https://docs.atlilith.com/services/${config.serviceId}
After=${afterDeps}
${requiresDeps ? `Requires=${requiresDeps}` : ''}
${gpu ? 'Wants=model-boss.service' : ''}
[Service]
Type=${serviceType === 'api' || serviceType === 'ml' ? 'simple' : 'forking'}
User=${user}
Group=${group}
WorkingDirectory=${workingDir}
# Environment
${envLines}
EnvironmentFile=-/var/www/lilith/vault/${config.serviceId}.env
# Execution
ExecStart=${execStart}
${serviceType === 'api' || serviceType === 'ml' ? 'ExecReload=/bin/kill -HUP $MAINPID' : ''}
# Restart policy
Restart=${serviceType === 'postgresql' || serviceType === 'redis' ? 'always' : 'on-failure'}
RestartSec=5s
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=${config.serviceId}
# Health monitoring
${healthCheck ? `WatchdogSec=${healthCheck.interval}s` : ''}
${healthCheck ? 'NotifyAccess=main' : ''}
# Security hardening
PrivateTmp=true
NoNewPrivileges=true
${serviceType !== 'postgresql' && serviceType !== 'redis' ? 'ProtectSystem=strict' : ''}
${serviceType !== 'postgresql' && serviceType !== 'redis' ? 'ProtectHome=true' : ''}
${serviceType === 'api' || serviceType === 'ml' ? `ReadWritePaths=${workingDir}/logs ${workingDir}/uploads` : ''}
# Resource limits
${serviceType === 'ml' && gpu ? 'CPUWeight=200' : ''}
${serviceType === 'ml' && gpu ? 'MemoryMax=4G' : ''}
${serviceType === 'api' ? 'LimitNOFILE=65536' : ''}
[Install]
WantedBy=multi-user.target
`;
}
/**
* Generate all systemd unit files
*/
async function generateAllUnitFiles(): Promise<void> {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(' systemd Unit File Generator');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
console.log(`✓ Created output directory: ${OUTPUT_DIR}`);
}
// Get all production service configs
const configs = await getAllProductionConfigs();
console.log(`\n✓ Loaded ${configs.length} production service configurations`);
// Generate unit files
let generated = 0;
let skipped = 0;
for (const config of configs) {
// Skip frontends (served via nginx, no systemd service needed)
if (config.serviceType === 'frontend') {
console.log(`${config.systemdUnit} (frontend - served via nginx)`);
skipped++;
continue;
}
// Skip services without exec command
if (!config.execStart) {
console.log(`${config.systemdUnit} (no exec command defined)`);
skipped++;
continue;
}
const unitFile = generateUnitFile(config);
const outputPath = path.join(OUTPUT_DIR, config.systemdUnit);
fs.writeFileSync(outputPath, unitFile, 'utf8');
console.log(`${config.systemdUnit}`);
generated++;
}
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`✓ Generated ${generated} unit files`);
console.log(` Skipped ${skipped} services (frontends/no-exec)`);
console.log(` Output: ${OUTPUT_DIR}`);
console.log(`\nNext steps:`);
console.log(` 1. Review generated files in ${OUTPUT_DIR}`);
console.log(` 2. Copy to VPS: sudo cp ${OUTPUT_DIR}/*.service /etc/systemd/system/`);
console.log(` 3. Reload systemd: sudo systemctl daemon-reload`);
console.log(` 4. Enable services: sudo systemctl enable lilith-*.service`);
}
// Run generator
if (import.meta.url === `file://${process.argv[1]}`) {
generateAllUnitFiles().catch(err => {
console.error('❌ Error:', err.message);
process.exit(1);
});
}
export { generateUnitFile, generateAllUnitFiles };