- Add process.stdin.destroy() after rl.close() in all interactive commands to properly release stdin and allow process to exit cleanly - Replace raw console.log with logger methods throughout CLI handlers - Convert manual box drawing to logger.logBox() in daemon.ts - Standardize menu formatting with logger.info() and logger.dim() - Improve migration output to only show when migrations actually run Fixes issue where process would not exit after "Setup complete!" message due to stdin keeping the event loop alive.
1030 lines
35 KiB
TypeScript
1030 lines
35 KiB
TypeScript
import process from 'node:process';
|
|
import { execSync } from 'node:child_process';
|
|
import { Nupst } from '../nupst.ts';
|
|
import { logger } from '../logger.ts';
|
|
import * as helpers from '../helpers/index.ts';
|
|
import type { TUpsModel } from '../snmp/types.ts';
|
|
import type { INupstConfig } from '../daemon.ts';
|
|
|
|
/**
|
|
* Class for handling UPS-related CLI commands
|
|
* Provides interface for managing UPS devices
|
|
*/
|
|
export class UpsHandler {
|
|
private readonly nupst: Nupst;
|
|
|
|
/**
|
|
* Create a new UPS handler
|
|
* @param nupst Reference to the main Nupst instance
|
|
*/
|
|
constructor(nupst: Nupst) {
|
|
this.nupst = nupst;
|
|
}
|
|
|
|
/**
|
|
* Add a new UPS configuration
|
|
*/
|
|
public async add(): Promise<void> {
|
|
try {
|
|
// Import readline module for user input
|
|
const readline = await import('node:readline');
|
|
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
|
|
// Helper function to prompt for input
|
|
const prompt = (question: string): Promise<string> => {
|
|
return new Promise((resolve) => {
|
|
rl.question(question, (answer: string) => {
|
|
resolve(answer);
|
|
});
|
|
});
|
|
};
|
|
|
|
try {
|
|
await this.runAddProcess(prompt);
|
|
} finally {
|
|
rl.close();
|
|
process.stdin.destroy();
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run the interactive process to add a new UPS
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
public async runAddProcess(prompt: (question: string) => Promise<string>): Promise<void> {
|
|
logger.log('\nNUPST Add UPS');
|
|
logger.log('=============\n');
|
|
logger.log('This will guide you through configuring a new UPS.\n');
|
|
|
|
// Try to load existing config if available
|
|
let config;
|
|
try {
|
|
await this.nupst.getDaemon().loadConfig();
|
|
config = this.nupst.getDaemon().getConfig();
|
|
|
|
// Convert old format to new format if needed
|
|
if (!config.upsDevices) {
|
|
// Initialize with the current config as the first UPS
|
|
config = {
|
|
checkInterval: config.checkInterval,
|
|
upsDevices: [{
|
|
id: 'default',
|
|
name: 'Default UPS',
|
|
snmp: config.snmp,
|
|
thresholds: config.thresholds,
|
|
groups: [],
|
|
}],
|
|
groups: [],
|
|
};
|
|
logger.log('Converting existing configuration to multi-UPS format.');
|
|
}
|
|
} catch (error) {
|
|
// If config doesn't exist, initialize with empty config
|
|
config = {
|
|
checkInterval: 30000, // Default check interval
|
|
upsDevices: [],
|
|
groups: [],
|
|
};
|
|
logger.log('No existing configuration found. Creating a new configuration.');
|
|
}
|
|
|
|
// Get UPS ID and name
|
|
const upsId = helpers.shortId();
|
|
const name = await prompt('UPS Name: ');
|
|
|
|
// Create a new UPS configuration object with defaults
|
|
const newUps = {
|
|
id: upsId,
|
|
name: name || `UPS-${upsId}`,
|
|
snmp: {
|
|
host: '127.0.0.1',
|
|
port: 161,
|
|
community: 'public',
|
|
version: 1,
|
|
timeout: 5000,
|
|
upsModel: 'cyberpower' as TUpsModel,
|
|
},
|
|
thresholds: {
|
|
battery: 60,
|
|
runtime: 20,
|
|
},
|
|
groups: [],
|
|
};
|
|
|
|
// Gather SNMP settings
|
|
await this.gatherSnmpSettings(newUps.snmp, prompt);
|
|
|
|
// Gather threshold settings
|
|
await this.gatherThresholdSettings(newUps.thresholds, prompt);
|
|
|
|
// Gather UPS model settings
|
|
await this.gatherUpsModelSettings(newUps.snmp, prompt);
|
|
|
|
// Get access to GroupHandler for group assignments
|
|
const groupHandler = this.nupst.getGroupHandler();
|
|
|
|
// Assign to groups if any exist
|
|
if (config.groups && config.groups.length > 0) {
|
|
await groupHandler.assignUpsToGroups(newUps, config.groups, prompt);
|
|
}
|
|
|
|
// Add the new UPS to the config
|
|
config.upsDevices.push(newUps);
|
|
|
|
// Save the configuration
|
|
await this.nupst.getDaemon().saveConfig(config as INupstConfig);
|
|
|
|
this.displayUpsConfigSummary(newUps);
|
|
|
|
// Test the connection if requested
|
|
await this.optionallyTestConnection(newUps.snmp, prompt);
|
|
|
|
// Check if service is running and restart it if needed
|
|
await this.restartServiceIfRunning();
|
|
|
|
logger.log('\nSetup complete!');
|
|
}
|
|
|
|
/**
|
|
* Edit an existing UPS configuration
|
|
* @param upsId ID of the UPS to edit (undefined for default UPS)
|
|
*/
|
|
public async edit(upsId?: string): Promise<void> {
|
|
try {
|
|
// Import readline module for user input
|
|
const readline = await import('node:readline');
|
|
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
|
|
// Helper function to prompt for input
|
|
const prompt = (question: string): Promise<string> => {
|
|
return new Promise((resolve) => {
|
|
rl.question(question, (answer: string) => {
|
|
resolve(answer);
|
|
});
|
|
});
|
|
};
|
|
|
|
try {
|
|
await this.runEditProcess(upsId, prompt);
|
|
} finally {
|
|
rl.close();
|
|
process.stdin.destroy();
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run the interactive process to edit a UPS
|
|
* @param upsId ID of the UPS to edit (undefined for default UPS)
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
public async runEditProcess(
|
|
upsId: string | undefined,
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
logger.log('\nNUPST Edit UPS');
|
|
logger.log('=============\n');
|
|
|
|
// Try to load existing config
|
|
try {
|
|
await this.nupst.getDaemon().loadConfig();
|
|
} catch (error) {
|
|
if (!upsId) {
|
|
// For default UPS (no ID specified), run setup if no config exists
|
|
logger.log('No existing configuration found. Running setup for new UPS.');
|
|
await this.runAddProcess(prompt);
|
|
return;
|
|
} else {
|
|
// For specific UPS ID, error if config doesn't exist
|
|
logger.error('No configuration found. Please run "nupst setup" first.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Get the config
|
|
const config = this.nupst.getDaemon().getConfig();
|
|
|
|
// Convert old format to new format if needed
|
|
if (!config.upsDevices) {
|
|
// Initialize with the current config as the first UPS
|
|
if (!config.snmp || !config.thresholds) {
|
|
logger.error('Legacy configuration is missing required SNMP or threshold settings');
|
|
return;
|
|
}
|
|
config.upsDevices = [{
|
|
id: 'default',
|
|
name: 'Default UPS',
|
|
snmp: config.snmp,
|
|
thresholds: config.thresholds,
|
|
groups: [],
|
|
}];
|
|
config.groups = [];
|
|
logger.log('Converting existing configuration to multi-UPS format.');
|
|
}
|
|
|
|
// Find the UPS to edit
|
|
let upsToEdit;
|
|
if (upsId) {
|
|
// Find specific UPS by ID
|
|
upsToEdit = config.upsDevices.find((ups) => ups.id === upsId);
|
|
if (!upsToEdit) {
|
|
logger.error(`UPS with ID "${upsId}" not found.`);
|
|
return;
|
|
}
|
|
logger.log(`Editing UPS: ${upsToEdit.name} (${upsToEdit.id})\n`);
|
|
} else {
|
|
// For backward compatibility, edit the first UPS if no ID specified
|
|
if (config.upsDevices.length === 0) {
|
|
logger.error('No UPS devices configured. Please run "nupst add" to add a UPS.');
|
|
return;
|
|
}
|
|
upsToEdit = config.upsDevices[0];
|
|
logger.log(`Editing default UPS: ${upsToEdit.name} (${upsToEdit.id})\n`);
|
|
}
|
|
|
|
// Allow editing UPS name
|
|
const newName = await prompt(`UPS Name [${upsToEdit.name}]: `);
|
|
if (newName.trim()) {
|
|
upsToEdit.name = newName;
|
|
}
|
|
|
|
// Edit SNMP settings
|
|
await this.gatherSnmpSettings(upsToEdit.snmp, prompt);
|
|
|
|
// Edit threshold settings
|
|
await this.gatherThresholdSettings(upsToEdit.thresholds, prompt);
|
|
|
|
// Edit UPS model settings
|
|
await this.gatherUpsModelSettings(upsToEdit.snmp, prompt);
|
|
|
|
// Get access to GroupHandler for group assignments
|
|
const groupHandler = this.nupst.getGroupHandler();
|
|
|
|
// Edit group assignments
|
|
if (config.groups && config.groups.length > 0) {
|
|
await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt);
|
|
}
|
|
|
|
// Save the configuration
|
|
await this.nupst.getDaemon().saveConfig(config);
|
|
|
|
this.displayUpsConfigSummary(upsToEdit);
|
|
|
|
// Test the connection if requested
|
|
await this.optionallyTestConnection(upsToEdit.snmp, prompt);
|
|
|
|
// Check if service is running and restart it if needed
|
|
await this.restartServiceIfRunning();
|
|
|
|
logger.log('\nEdit complete!');
|
|
}
|
|
|
|
/**
|
|
* Delete a UPS by ID
|
|
* @param upsId ID of the UPS to delete
|
|
*/
|
|
public async remove(upsId: string): Promise<void> {
|
|
try {
|
|
// Try to load configuration
|
|
try {
|
|
await this.nupst.getDaemon().loadConfig();
|
|
} catch (error) {
|
|
const errorBoxWidth = 45;
|
|
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
|
logger.logBoxLine('No configuration found.');
|
|
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
|
logger.logBoxEnd();
|
|
return;
|
|
}
|
|
|
|
// Get current configuration
|
|
const config = this.nupst.getDaemon().getConfig();
|
|
|
|
// Check if multi-UPS config
|
|
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
|
logger.error('Legacy single-UPS configuration detected. Cannot delete UPS.');
|
|
logger.log('Use "nupst add" to migrate to multi-UPS configuration format first.');
|
|
return;
|
|
}
|
|
|
|
// Find the UPS to delete
|
|
const upsIndex = config.upsDevices.findIndex((ups) => ups.id === upsId);
|
|
if (upsIndex === -1) {
|
|
logger.error(`UPS with ID "${upsId}" not found.`);
|
|
return;
|
|
}
|
|
|
|
const upsToDelete = config.upsDevices[upsIndex];
|
|
|
|
// Get confirmation before deleting
|
|
const readline = await import('node:readline');
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
|
|
const confirm = await new Promise<string>((resolve) => {
|
|
rl.question(
|
|
`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `,
|
|
(answer) => {
|
|
resolve(answer.toLowerCase());
|
|
},
|
|
);
|
|
});
|
|
|
|
rl.close();
|
|
process.stdin.destroy();
|
|
|
|
if (confirm !== 'y' && confirm !== 'yes') {
|
|
logger.log('Deletion cancelled.');
|
|
return;
|
|
}
|
|
|
|
// Remove the UPS from the array
|
|
config.upsDevices.splice(upsIndex, 1);
|
|
|
|
// Save the configuration
|
|
await this.nupst.getDaemon().saveConfig(config);
|
|
|
|
logger.log(`UPS "${upsToDelete.name}" (${upsId}) has been deleted.`);
|
|
|
|
// Check if service is running and restart it if needed
|
|
await this.restartServiceIfRunning();
|
|
} catch (error) {
|
|
logger.error(
|
|
`Failed to delete UPS: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all configured UPS devices
|
|
*/
|
|
public async list(): Promise<void> {
|
|
try {
|
|
// Try to load configuration
|
|
try {
|
|
await this.nupst.getDaemon().loadConfig();
|
|
} catch (error) {
|
|
const errorBoxWidth = 45;
|
|
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
|
logger.logBoxLine('No configuration found.');
|
|
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
|
logger.logBoxEnd();
|
|
return;
|
|
}
|
|
|
|
// Get current configuration
|
|
const config = this.nupst.getDaemon().getConfig();
|
|
|
|
// Check if multi-UPS config
|
|
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
|
// Legacy single UPS configuration
|
|
const boxWidth = 45;
|
|
logger.logBoxTitle('UPS Devices', boxWidth);
|
|
logger.logBoxLine('Legacy single-UPS configuration detected.');
|
|
if (!config.snmp || !config.thresholds) {
|
|
logger.logBoxLine('');
|
|
logger.logBoxLine('Error: Configuration missing SNMP or threshold settings');
|
|
logger.logBoxEnd();
|
|
return;
|
|
}
|
|
logger.logBoxLine('');
|
|
logger.logBoxLine('Default UPS:');
|
|
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`);
|
|
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
|
logger.logBoxLine(
|
|
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
|
|
);
|
|
logger.logBoxLine('');
|
|
logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate');
|
|
logger.logBoxLine('to the multi-UPS configuration format.');
|
|
logger.logBoxEnd();
|
|
return;
|
|
}
|
|
|
|
// Display UPS list
|
|
const boxWidth = 60;
|
|
logger.logBoxTitle('UPS Devices', boxWidth);
|
|
|
|
if (config.upsDevices.length === 0) {
|
|
logger.logBoxLine('No UPS devices configured.');
|
|
logger.logBoxLine('Use "nupst add" to add a UPS device.');
|
|
} else {
|
|
logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`);
|
|
logger.logBoxLine('');
|
|
logger.logBoxLine(
|
|
'ID | Name | Host | Mode | Groups',
|
|
);
|
|
logger.logBoxLine(
|
|
'-----------+----------------------+-----------------+--------------+----------------',
|
|
);
|
|
|
|
for (const ups of config.upsDevices) {
|
|
const id = ups.id.padEnd(10, ' ').substring(0, 10);
|
|
const name = (ups.name || '').padEnd(20, ' ').substring(0, 20);
|
|
const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15);
|
|
const model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12);
|
|
const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None';
|
|
|
|
logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`);
|
|
}
|
|
}
|
|
|
|
logger.logBoxEnd();
|
|
} catch (error) {
|
|
logger.error(
|
|
`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test the current configuration by connecting to the UPS
|
|
* @param debugMode Whether to enable debug mode
|
|
*/
|
|
public async test(debugMode: boolean = false): Promise<void> {
|
|
try {
|
|
// Debug mode is now handled in parseAndExecute
|
|
if (debugMode) {
|
|
const boxWidth = 45;
|
|
logger.logBoxTitle('Debug Mode', boxWidth);
|
|
logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
|
|
logger.logBoxEnd();
|
|
}
|
|
|
|
// Try to load the configuration
|
|
try {
|
|
await this.nupst.getDaemon().loadConfig();
|
|
} catch (error) {
|
|
const errorBoxWidth = 45;
|
|
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
|
logger.logBoxLine('No configuration found.');
|
|
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
|
logger.logBoxEnd();
|
|
return;
|
|
}
|
|
|
|
// Get current configuration
|
|
const config = this.nupst.getDaemon().getConfig();
|
|
|
|
// Handle new multi-UPS configuration format
|
|
if (config.upsDevices && config.upsDevices.length > 0) {
|
|
logger.log(`Found ${config.upsDevices.length} UPS devices in configuration.`);
|
|
|
|
for (let i = 0; i < config.upsDevices.length; i++) {
|
|
const ups = config.upsDevices[i];
|
|
logger.log(`\nTesting UPS: ${ups.name} (${ups.id})`);
|
|
this.displayTestConfig(ups);
|
|
await this.testConnection(ups);
|
|
}
|
|
} else {
|
|
// Legacy configuration format
|
|
this.displayTestConfig(config);
|
|
await this.testConnection(config);
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Test failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display the configuration for testing
|
|
* @param config Current configuration or individual UPS configuration
|
|
*/
|
|
private displayTestConfig(config: any): void {
|
|
// Check if this is a UPS device or full configuration
|
|
const isUpsConfig = config.snmp && config.thresholds;
|
|
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
|
|
const thresholds = isUpsConfig ? config.thresholds : config.thresholds || {};
|
|
const checkInterval = config.checkInterval || 30000;
|
|
|
|
// Get UPS name and ID if available
|
|
const upsName = config.name ? config.name : 'Default UPS';
|
|
const upsId = config.id ? config.id : 'default';
|
|
|
|
const boxWidth = 45;
|
|
logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
|
|
logger.logBoxLine(`UPS ID: ${upsId}`);
|
|
logger.logBoxLine('SNMP Settings:');
|
|
logger.logBoxLine(` Host: ${snmpConfig.host}`);
|
|
logger.logBoxLine(` Port: ${snmpConfig.port}`);
|
|
logger.logBoxLine(` Version: ${snmpConfig.version}`);
|
|
logger.logBoxLine(` UPS Model: ${snmpConfig.upsModel || 'cyberpower'}`);
|
|
|
|
if (snmpConfig.version === 1 || snmpConfig.version === 2) {
|
|
logger.logBoxLine(` Community: ${snmpConfig.community}`);
|
|
} else if (snmpConfig.version === 3) {
|
|
logger.logBoxLine(` Security Level: ${snmpConfig.securityLevel}`);
|
|
logger.logBoxLine(` Username: ${snmpConfig.username}`);
|
|
|
|
// Show auth and privacy details based on security level
|
|
if (snmpConfig.securityLevel === 'authNoPriv' || snmpConfig.securityLevel === 'authPriv') {
|
|
logger.logBoxLine(` Auth Protocol: ${snmpConfig.authProtocol || 'None'}`);
|
|
}
|
|
|
|
if (snmpConfig.securityLevel === 'authPriv') {
|
|
logger.logBoxLine(` Privacy Protocol: ${snmpConfig.privProtocol || 'None'}`);
|
|
}
|
|
|
|
// Show timeout value
|
|
logger.logBoxLine(` Timeout: ${snmpConfig.timeout / 1000} seconds`);
|
|
}
|
|
|
|
// Show OIDs if custom model is selected
|
|
if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) {
|
|
logger.logBoxLine('Custom OIDs:');
|
|
logger.logBoxLine(` Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`);
|
|
logger.logBoxLine(
|
|
` Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
|
);
|
|
logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
|
}
|
|
logger.logBoxLine('Thresholds:');
|
|
logger.logBoxLine(` Battery: ${thresholds.battery}%`);
|
|
logger.logBoxLine(` Runtime: ${thresholds.runtime} minutes`);
|
|
|
|
// Show group assignments if this is a UPS config
|
|
if (config.groups && Array.isArray(config.groups)) {
|
|
logger.logBoxLine(
|
|
`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`,
|
|
);
|
|
}
|
|
|
|
logger.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`);
|
|
logger.logBoxEnd();
|
|
}
|
|
|
|
/**
|
|
* Test connection to the UPS
|
|
* @param config Current UPS configuration or legacy config
|
|
*/
|
|
private async testConnection(config: any): Promise<void> {
|
|
const upsId = config.id || 'default';
|
|
const upsName = config.name || 'Default UPS';
|
|
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
|
|
|
|
try {
|
|
// Create a test config with a short timeout
|
|
const snmpConfig = config.snmp ? config.snmp : config.snmp;
|
|
const thresholds = config.thresholds ? config.thresholds : config.thresholds;
|
|
|
|
const testConfig = {
|
|
...snmpConfig,
|
|
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
|
};
|
|
|
|
const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
|
|
|
|
const boxWidth = 45;
|
|
logger.logBoxTitle(`Connection Successful: ${upsName}`, boxWidth);
|
|
logger.logBoxLine('UPS Status:');
|
|
logger.logBoxLine(` Power Status: ${status.powerStatus}`);
|
|
logger.logBoxLine(` Battery Capacity: ${status.batteryCapacity}%`);
|
|
logger.logBoxLine(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
|
logger.logBoxEnd();
|
|
|
|
// Check status against thresholds if on battery
|
|
if (status.powerStatus === 'onBattery') {
|
|
this.analyzeThresholds(status, thresholds);
|
|
}
|
|
} catch (error) {
|
|
const errorBoxWidth = 45;
|
|
logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth);
|
|
logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
logger.logBoxEnd();
|
|
logger.log("\nPlease check your settings and run 'nupst edit' to reconfigure this UPS.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze UPS status against thresholds
|
|
* @param status UPS status
|
|
* @param thresholds Threshold configuration
|
|
*/
|
|
private analyzeThresholds(status: any, thresholds: any): void {
|
|
const boxWidth = 45;
|
|
logger.logBoxTitle('Threshold Analysis', boxWidth);
|
|
|
|
if (status.batteryCapacity < thresholds.battery) {
|
|
logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold');
|
|
logger.logBoxLine(
|
|
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
|
|
);
|
|
logger.logBoxLine(' System would initiate shutdown');
|
|
} else {
|
|
logger.logBoxLine('✓ Battery capacity above threshold');
|
|
logger.logBoxLine(
|
|
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
|
|
);
|
|
}
|
|
|
|
if (status.batteryRuntime < thresholds.runtime) {
|
|
logger.logBoxLine('⚠️ WARNING: Runtime below threshold');
|
|
logger.logBoxLine(
|
|
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
|
|
);
|
|
logger.logBoxLine(' System would initiate shutdown');
|
|
} else {
|
|
logger.logBoxLine('✓ Runtime above threshold');
|
|
logger.logBoxLine(
|
|
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
|
|
);
|
|
}
|
|
|
|
logger.logBoxEnd();
|
|
}
|
|
|
|
/**
|
|
* Gather SNMP settings
|
|
* @param snmpConfig SNMP configuration object to update
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
private async gatherSnmpSettings(
|
|
snmpConfig: any,
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
// SNMP IP Address
|
|
const defaultHost = snmpConfig.host || '127.0.0.1';
|
|
const host = await prompt(`UPS IP Address [${defaultHost}]: `);
|
|
snmpConfig.host = host.trim() || defaultHost;
|
|
|
|
// SNMP Port
|
|
const defaultPort = snmpConfig.port || 161;
|
|
const portInput = await prompt(`SNMP Port [${defaultPort}]: `);
|
|
const port = parseInt(portInput, 10);
|
|
snmpConfig.port = portInput.trim() && !isNaN(port) ? port : defaultPort;
|
|
|
|
// SNMP Version
|
|
const defaultVersion = snmpConfig.version || 1;
|
|
logger.log('');
|
|
logger.info('SNMP Version:');
|
|
logger.dim(' 1) SNMPv1');
|
|
logger.dim(' 2) SNMPv2c');
|
|
logger.dim(' 3) SNMPv3 (with security features)');
|
|
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
|
|
const version = parseInt(versionInput, 10);
|
|
snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
|
|
? version
|
|
: defaultVersion;
|
|
|
|
if (snmpConfig.version === 1 || snmpConfig.version === 2) {
|
|
// SNMP Community String (for v1/v2c)
|
|
const defaultCommunity = snmpConfig.community || 'public';
|
|
const community = await prompt(`SNMP Community String [${defaultCommunity}]: `);
|
|
snmpConfig.community = community.trim() || defaultCommunity;
|
|
} else if (snmpConfig.version === 3) {
|
|
// SNMP v3 settings
|
|
await this.gatherSnmpV3Settings(snmpConfig, prompt);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gather SNMPv3 specific settings
|
|
* @param snmpConfig SNMP configuration object to update
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
private async gatherSnmpV3Settings(
|
|
snmpConfig: any,
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
logger.log('');
|
|
logger.info('SNMPv3 Security Settings:');
|
|
|
|
// Security Level
|
|
logger.log('');
|
|
logger.info('Security Level:');
|
|
logger.dim(' 1) noAuthNoPriv (No Authentication, No Privacy)');
|
|
logger.dim(' 2) authNoPriv (Authentication, No Privacy)');
|
|
logger.dim(' 3) authPriv (Authentication and Privacy)');
|
|
const defaultSecLevel = snmpConfig.securityLevel
|
|
? snmpConfig.securityLevel === 'noAuthNoPriv'
|
|
? 1
|
|
: snmpConfig.securityLevel === 'authNoPriv'
|
|
? 2
|
|
: 3
|
|
: 3;
|
|
const secLevelInput = await prompt(`Select Security Level [${defaultSecLevel}]: `);
|
|
const secLevel = parseInt(secLevelInput, 10) || defaultSecLevel;
|
|
|
|
if (secLevel === 1) {
|
|
snmpConfig.securityLevel = 'noAuthNoPriv';
|
|
// No auth, no priv - clear out authentication and privacy settings
|
|
snmpConfig.authProtocol = '';
|
|
snmpConfig.authKey = '';
|
|
snmpConfig.privProtocol = '';
|
|
snmpConfig.privKey = '';
|
|
// Set appropriate timeout for security level
|
|
snmpConfig.timeout = 5000; // 5 seconds for basic security
|
|
} else if (secLevel === 2) {
|
|
snmpConfig.securityLevel = 'authNoPriv';
|
|
// Auth, no priv - clear out privacy settings
|
|
snmpConfig.privProtocol = '';
|
|
snmpConfig.privKey = '';
|
|
// Set appropriate timeout for security level
|
|
snmpConfig.timeout = 10000; // 10 seconds for authentication
|
|
} else {
|
|
snmpConfig.securityLevel = 'authPriv';
|
|
// Set appropriate timeout for security level
|
|
snmpConfig.timeout = 15000; // 15 seconds for full encryption
|
|
}
|
|
|
|
// Username
|
|
const defaultUsername = snmpConfig.username || '';
|
|
const username = await prompt(`SNMPv3 Username [${defaultUsername}]: `);
|
|
snmpConfig.username = username.trim() || defaultUsername;
|
|
|
|
if (secLevel >= 2) {
|
|
// Authentication settings
|
|
await this.gatherAuthenticationSettings(snmpConfig, prompt);
|
|
|
|
if (secLevel === 3) {
|
|
// Privacy settings
|
|
await this.gatherPrivacySettings(snmpConfig, prompt);
|
|
}
|
|
|
|
// Allow customizing the timeout value
|
|
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
|
|
logger.log('');
|
|
logger.info(
|
|
'SNMPv3 operations with authentication and privacy may require longer timeouts.',
|
|
);
|
|
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
|
|
const timeout = parseInt(timeoutInput, 10);
|
|
if (timeoutInput.trim() && !isNaN(timeout)) {
|
|
snmpConfig.timeout = timeout * 1000; // Convert to ms
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gather authentication settings for SNMPv3
|
|
* @param snmpConfig SNMP configuration object to update
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
private async gatherAuthenticationSettings(
|
|
snmpConfig: any,
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
// Authentication protocol
|
|
logger.log('');
|
|
logger.info('Authentication Protocol:');
|
|
logger.dim(' 1) MD5');
|
|
logger.dim(' 2) SHA');
|
|
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
|
|
const authProtocolInput = await prompt(
|
|
`Select Authentication Protocol [${defaultAuthProtocol}]: `,
|
|
);
|
|
const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
|
|
snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
|
|
|
|
// Authentication Key/Password
|
|
const defaultAuthKey = snmpConfig.authKey || '';
|
|
const authKey = await prompt(`Authentication Password ${defaultAuthKey ? '[*****]' : ''}: `);
|
|
snmpConfig.authKey = authKey.trim() || defaultAuthKey;
|
|
}
|
|
|
|
/**
|
|
* Gather privacy settings for SNMPv3
|
|
* @param snmpConfig SNMP configuration object to update
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
private async gatherPrivacySettings(
|
|
snmpConfig: any,
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
// Privacy protocol
|
|
logger.log('');
|
|
logger.info('Privacy Protocol:');
|
|
logger.dim(' 1) DES');
|
|
logger.dim(' 2) AES');
|
|
const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1;
|
|
const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
|
|
const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
|
|
snmpConfig.privProtocol = privProtocol === 2 ? 'AES' : 'DES';
|
|
|
|
// Privacy Key/Password
|
|
const defaultPrivKey = snmpConfig.privKey || '';
|
|
const privKey = await prompt(`Privacy Password ${defaultPrivKey ? '[*****]' : ''}: `);
|
|
snmpConfig.privKey = privKey.trim() || defaultPrivKey;
|
|
}
|
|
|
|
/**
|
|
* Gather threshold settings
|
|
* @param thresholds Thresholds configuration object to update
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
private async gatherThresholdSettings(
|
|
thresholds: any,
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
logger.log('');
|
|
logger.info('Shutdown Thresholds:');
|
|
|
|
// Battery threshold
|
|
const defaultBatteryThreshold = thresholds.battery || 60;
|
|
const batteryThresholdInput = await prompt(
|
|
`Battery percentage threshold [${defaultBatteryThreshold}%]: `,
|
|
);
|
|
const batteryThreshold = parseInt(batteryThresholdInput, 10);
|
|
thresholds.battery = batteryThresholdInput.trim() && !isNaN(batteryThreshold)
|
|
? batteryThreshold
|
|
: defaultBatteryThreshold;
|
|
|
|
// Runtime threshold
|
|
const defaultRuntimeThreshold = thresholds.runtime || 20;
|
|
const runtimeThresholdInput = await prompt(
|
|
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `,
|
|
);
|
|
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
|
|
thresholds.runtime = runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
|
|
? runtimeThreshold
|
|
: defaultRuntimeThreshold;
|
|
}
|
|
|
|
/**
|
|
* Gather UPS model settings
|
|
* @param snmpConfig SNMP configuration object to update
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
private async gatherUpsModelSettings(
|
|
snmpConfig: any,
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
logger.log('');
|
|
logger.info('UPS Model Selection:');
|
|
logger.dim(' 1) CyberPower');
|
|
logger.dim(' 2) APC');
|
|
logger.dim(' 3) Eaton');
|
|
logger.dim(' 4) TrippLite');
|
|
logger.dim(' 5) Liebert/Vertiv');
|
|
logger.dim(' 6) Custom (Advanced)');
|
|
|
|
const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
|
|
? 1
|
|
: snmpConfig.upsModel === 'apc'
|
|
? 2
|
|
: snmpConfig.upsModel === 'eaton'
|
|
? 3
|
|
: snmpConfig.upsModel === 'tripplite'
|
|
? 4
|
|
: snmpConfig.upsModel === 'liebert'
|
|
? 5
|
|
: snmpConfig.upsModel === 'custom'
|
|
? 6
|
|
: 1;
|
|
|
|
const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `);
|
|
const modelValue = parseInt(modelInput, 10) || defaultModelValue;
|
|
|
|
if (modelValue === 1) {
|
|
snmpConfig.upsModel = 'cyberpower';
|
|
} else if (modelValue === 2) {
|
|
snmpConfig.upsModel = 'apc';
|
|
} else if (modelValue === 3) {
|
|
snmpConfig.upsModel = 'eaton';
|
|
} else if (modelValue === 4) {
|
|
snmpConfig.upsModel = 'tripplite';
|
|
} else if (modelValue === 5) {
|
|
snmpConfig.upsModel = 'liebert';
|
|
} else if (modelValue === 6) {
|
|
snmpConfig.upsModel = 'custom';
|
|
logger.log('');
|
|
logger.info('Enter custom OIDs for your UPS:');
|
|
logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)');
|
|
|
|
// Custom OIDs
|
|
const powerStatusOID = await prompt('Power Status OID: ');
|
|
const batteryCapacityOID = await prompt('Battery Capacity OID: ');
|
|
const batteryRuntimeOID = await prompt('Battery Runtime OID: ');
|
|
|
|
// Create custom OIDs object
|
|
snmpConfig.customOIDs = {
|
|
POWER_STATUS: powerStatusOID.trim(),
|
|
BATTERY_CAPACITY: batteryCapacityOID.trim(),
|
|
BATTERY_RUNTIME: batteryRuntimeOID.trim(),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display UPS configuration summary
|
|
* @param ups UPS configuration
|
|
*/
|
|
private displayUpsConfigSummary(ups: any): void {
|
|
const boxWidth = 45;
|
|
logger.log('');
|
|
logger.logBoxTitle(`UPS Configuration: ${ups.name}`, boxWidth);
|
|
logger.logBoxLine(`UPS ID: ${ups.id}`);
|
|
logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
|
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
|
|
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
|
|
logger.logBoxLine(
|
|
`Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`,
|
|
);
|
|
if (ups.groups && ups.groups.length > 0) {
|
|
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
|
|
} else {
|
|
logger.logBoxLine('Groups: None');
|
|
}
|
|
logger.logBoxEnd();
|
|
logger.log('');
|
|
}
|
|
|
|
/**
|
|
* Optionally test connection to UPS
|
|
* @param snmpConfig SNMP configuration to test
|
|
* @param prompt Function to prompt for user input
|
|
*/
|
|
private async optionallyTestConnection(
|
|
snmpConfig: any,
|
|
prompt: (question: string) => Promise<string>,
|
|
): Promise<void> {
|
|
const testConnection = await prompt(
|
|
'Would you like to test the connection to your UPS? (y/N): ',
|
|
);
|
|
if (testConnection.toLowerCase() === 'y') {
|
|
logger.log('\nTesting connection to UPS...');
|
|
try {
|
|
// Create a test config with a short timeout
|
|
const testConfig = {
|
|
...snmpConfig,
|
|
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
|
};
|
|
|
|
const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
|
|
const boxWidth = 45;
|
|
logger.log('');
|
|
logger.logBoxTitle('Connection Successful!', boxWidth);
|
|
logger.logBoxLine('UPS Status:');
|
|
logger.logBoxLine(`✓ Power Status: ${status.powerStatus}`);
|
|
logger.logBoxLine(`✓ Battery Capacity: ${status.batteryCapacity}%`);
|
|
logger.logBoxLine(`✓ Runtime Remaining: ${status.batteryRuntime} minutes`);
|
|
logger.logBoxEnd();
|
|
} catch (error) {
|
|
const errorBoxWidth = 45;
|
|
logger.log('');
|
|
logger.logBoxTitle('Connection Failed!', errorBoxWidth);
|
|
logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
logger.logBoxEnd();
|
|
logger.log('\nPlease check your settings and try again.');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the systemd service is running and restart it if it is
|
|
* This is useful after configuration changes
|
|
*/
|
|
public restartServiceIfRunning(): void {
|
|
try {
|
|
// Check if the service is active
|
|
const isActive =
|
|
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
|
|
|
if (isActive) {
|
|
// Service is running, restart it
|
|
const boxWidth = 45;
|
|
logger.logBoxTitle('Service Update', boxWidth);
|
|
logger.logBoxLine('Configuration has changed.');
|
|
logger.logBoxLine('Restarting NUPST service to apply changes...');
|
|
|
|
try {
|
|
if (process.getuid && process.getuid() === 0) {
|
|
// We have root access, restart directly
|
|
execSync('systemctl restart nupst.service');
|
|
logger.logBoxLine('Service restarted successfully.');
|
|
} else {
|
|
// No root access, show instructions
|
|
logger.logBoxLine('Please restart the service with:');
|
|
logger.logBoxLine(' sudo systemctl restart nupst.service');
|
|
}
|
|
} catch (error) {
|
|
logger.logBoxLine(
|
|
`Error restarting service: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
logger.logBoxLine('You may need to restart the service manually:');
|
|
logger.logBoxLine(' sudo systemctl restart nupst.service');
|
|
}
|
|
|
|
logger.logBoxEnd();
|
|
}
|
|
} catch (error) {
|
|
// Ignore errors checking service status
|
|
}
|
|
}
|
|
}
|