nupst/ts/systemd.ts

343 lines
10 KiB
TypeScript

import { promises as fs } from 'fs';
import { execSync } from 'child_process';
import { NupstDaemon } from './daemon.js';
import { logger } from './logger.js';
/**
* Class for managing systemd service
* Handles installation, removal, and control of the NUPST systemd service
*/
export class NupstSystemd {
/** Path to the systemd service file */
private readonly serviceFilePath = '/etc/systemd/system/nupst.service';
private readonly daemon: NupstDaemon;
/** Template for the systemd service file */
private readonly serviceTemplate = `[Unit]
Description=Node.js UPS Shutdown Tool for Multiple UPS Devices
After=network.target
[Service]
ExecStart=/opt/nupst/bin/nupst daemon-start
Restart=always
User=root
Group=root
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/tmp
[Install]
WantedBy=multi-user.target
`;
/**
* Create a new systemd manager
* @param daemon The daemon instance to manage
*/
constructor(daemon: NupstDaemon) {
this.daemon = daemon;
}
/**
* Check if a configuration file exists
* @private
* @throws Error if configuration not found
*/
private async checkConfigExists(): Promise<void> {
const configPath = '/etc/nupst/config.json';
try {
await fs.access(configPath);
} catch (error) {
const boxWidth = 50;
logger.logBoxTitle('Configuration Error', boxWidth);
logger.logBoxLine(`No configuration file found at ${configPath}`);
logger.logBoxLine("Please run 'nupst add' first to create a UPS configuration.");
logger.logBoxEnd();
throw new Error('Configuration not found');
}
}
/**
* Install the systemd service file
* @throws Error if installation fails
*/
public async install(): Promise<void> {
try {
// Check if configuration exists before installing
await this.checkConfigExists();
// Write the service file
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
const boxWidth = 50;
logger.logBoxTitle('Service Installation', boxWidth);
logger.logBoxLine(`Service file created at ${this.serviceFilePath}`);
// Reload systemd daemon
execSync('systemctl daemon-reload');
logger.logBoxLine('Systemd daemon reloaded');
// Enable the service
execSync('systemctl enable nupst.service');
logger.logBoxLine('Service enabled to start on boot');
logger.logBoxEnd();
} catch (error) {
if (error.message === 'Configuration not found') {
// Just rethrow the error as the message has already been displayed
throw error;
}
logger.error(`Failed to install systemd service: ${error}`);
throw error;
}
}
/**
* Start the systemd service
* @throws Error if start fails
*/
public async start(): Promise<void> {
try {
// Check if configuration exists before starting
await this.checkConfigExists();
execSync('systemctl start nupst.service');
const boxWidth = 45;
logger.logBoxTitle('Service Status', boxWidth);
logger.logBoxLine('NUPST service started successfully');
logger.logBoxEnd();
} catch (error) {
if (error.message === 'Configuration not found') {
// Exit with error code since configuration is required
process.exit(1);
}
logger.error(`Failed to start service: ${error}`);
throw error;
}
}
/**
* Stop the systemd service
* @throws Error if stop fails
*/
public async stop(): Promise<void> {
try {
execSync('systemctl stop nupst.service');
logger.success('NUPST service stopped');
} catch (error) {
logger.error(`Failed to stop service: ${error}`);
throw error;
}
}
/**
* Get status of the systemd service and UPS
* @param debugMode Whether to enable debug mode for SNMP
*/
public async getStatus(debugMode: boolean = false): Promise<void> {
try {
// Enable debug mode if requested
if (debugMode) {
const boxWidth = 45;
logger.logBoxTitle('Debug Mode', boxWidth);
logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
logger.logBoxEnd();
this.daemon.getNupstSnmp().enableDebug();
}
// Display version information
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
// Check if config exists first
try {
await this.checkConfigExists();
} catch (error) {
// Error message already displayed by checkConfigExists
return;
}
await this.displayServiceStatus();
await this.displayAllUpsStatus();
} catch (error) {
logger.error(`Failed to get status: ${error.message}`);
}
}
/**
* Display the systemd service status
* @private
*/
private async displayServiceStatus(): Promise<void> {
try {
const serviceStatus = execSync('systemctl status nupst.service').toString();
const boxWidth = 45;
logger.logBoxTitle('Service Status', boxWidth);
// Process each line of the status output
serviceStatus.split('\n').forEach(line => {
logger.logBoxLine(line);
});
logger.logBoxEnd();
} catch (error) {
const boxWidth = 45;
logger.logBoxTitle('Service Status', boxWidth);
logger.logBoxLine('Service is not running');
logger.logBoxEnd();
}
}
/**
* Display all UPS statuses
* @private
*/
private async displayAllUpsStatus(): Promise<void> {
try {
// Explicitly load the configuration first to ensure it's up-to-date
await this.daemon.loadConfig();
const config = this.daemon.getConfig();
const snmp = this.daemon.getNupstSnmp();
// Check if we have the new multi-UPS config format
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`);
// Show status for each UPS
for (const ups of config.upsDevices) {
await this.displaySingleUpsStatus(ups, snmp);
}
} else if (config.snmp) {
// Legacy single UPS configuration
const legacyUps = {
id: 'default',
name: 'Default UPS',
snmp: config.snmp,
thresholds: config.thresholds,
groups: []
};
await this.displaySingleUpsStatus(legacyUps, snmp);
} else {
logger.error('No UPS devices found in configuration');
}
} catch (error) {
const boxWidth = 45;
logger.logBoxTitle('UPS Status', boxWidth);
logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
logger.logBoxEnd();
}
}
/**
* Display status of a single UPS
* @param ups UPS configuration
* @param snmp SNMP manager
*/
private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> {
const boxWidth = 45;
logger.logBoxTitle(`Connecting to UPS: ${ups.name}`, boxWidth);
logger.logBoxLine(`ID: ${ups.id}`);
logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`);
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`);
if (ups.groups && ups.groups.length > 0) {
// Get group names if available
const config = this.daemon.getConfig();
const groupNames = ups.groups.map(groupId => {
const group = config.groups?.find(g => g.id === groupId);
return group ? group.name : groupId;
});
logger.logBoxLine(`Groups: ${groupNames.join(', ')}`);
}
logger.logBoxEnd();
try {
// Create a test config with a short timeout
const testConfig = {
...ups.snmp,
timeout: Math.min(ups.snmp.timeout, 10000) // Use at most 10 seconds for status check
};
const status = await snmp.getUpsStatus(testConfig);
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`);
logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`);
// Show threshold status
logger.logBoxLine('');
logger.logBoxLine('Thresholds:');
logger.logBoxLine(` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
}`);
logger.logBoxLine(` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
}`);
logger.logBoxEnd();
} catch (error) {
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
logger.logBoxEnd();
}
}
/**
* Disable and uninstall the systemd service
* @throws Error if disabling fails
*/
public async disable(): Promise<void> {
try {
await this.stopService();
await this.disableService();
await this.removeServiceFile();
// Reload systemd daemon
execSync('systemctl daemon-reload');
logger.log('Systemd daemon reloaded');
logger.success('NUPST service has been successfully uninstalled');
} catch (error) {
logger.error(`Failed to disable and uninstall service: ${error}`);
throw error;
}
}
/**
* Stop the service if it's running
* @private
*/
private async stopService(): Promise<void> {
try {
logger.log('Stopping NUPST service...');
execSync('systemctl stop nupst.service');
} catch (error) {
// Service might not be running, that's okay
logger.log('Service was not running or could not be stopped');
}
}
/**
* Disable the service
* @private
*/
private async disableService(): Promise<void> {
try {
logger.log('Disabling NUPST service...');
execSync('systemctl disable nupst.service');
} catch (error) {
logger.log('Service was not enabled or could not be disabled');
}
}
/**
* Remove the service file if it exists
* @private
*/
private async removeServiceFile(): Promise<void> {
if (await fs.stat(this.serviceFilePath).catch(() => null)) {
logger.log(`Removing service file ${this.serviceFilePath}...`);
await fs.unlink(this.serviceFilePath);
logger.log('Service file removed');
} else {
logger.log('Service file did not exist');
}
}
}