343 lines
10 KiB
TypeScript
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');
|
|
}
|
|
}
|
|
} |