Compare commits

...

2 Commits

8 changed files with 1993 additions and 1970 deletions

View File

@ -1,5 +1,14 @@
# Changelog
## 2025-03-28 - 3.1.0 - feat(cli)
Refactor CLI commands to use dedicated handlers for UPS, group, and service management
- Extracted UPS-related CLI logic into a new UpsHandler
- Introduced GroupHandler to manage UPS groups commands
- Added ServiceHandler for systemd service operations
- Updated CLI routing in cli.ts to delegate commands to the new handlers
- Exposed getters for the new handlers in the Nupst class
## 2025-03-28 - 3.0.1 - fix(cli)
Simplify UPS ID generation by removing the redundant promptForUniqueUpsId function in the CLI module and replacing it with the shortId helper.

View File

@ -1,6 +1,6 @@
{
"name": "@serve.zone/nupst",
"version": "3.0.1",
"version": "3.1.0",
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
"main": "dist/index.js",
"bin": {

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/nupst',
version: '3.0.1',
version: '3.1.0',
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
}

2041
ts/cli.ts

File diff suppressed because it is too large Load Diff

565
ts/cli/group-handler.ts Normal file
View File

@ -0,0 +1,565 @@
import { Nupst } from '../nupst.js';
import { logger } from '../logger.js';
import * as helpers from '../helpers/index.js';
import { type IGroupConfig } from '../daemon.js';
/**
* Class for handling group-related CLI commands
* Provides interface for managing UPS groups
*/
export class GroupHandler {
private readonly nupst: Nupst;
/**
* Create a new Group handler
* @param nupst Reference to the main Nupst instance
*/
constructor(nupst: Nupst) {
this.nupst = nupst;
}
/**
* List all UPS groups
*/
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.groups || !Array.isArray(config.groups)) {
// Legacy or missing groups configuration
const boxWidth = 45;
logger.logBoxTitle('UPS Groups', boxWidth);
logger.logBoxLine('No groups configured.');
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
logger.logBoxEnd();
return;
}
// Display group list
const boxWidth = 60;
logger.logBoxTitle('UPS Groups', boxWidth);
if (config.groups.length === 0) {
logger.logBoxLine('No UPS groups configured.');
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
} else {
logger.logBoxLine(`Found ${config.groups.length} group(s)`);
logger.logBoxLine('');
logger.logBoxLine('ID | Name | Mode | UPS Devices');
logger.logBoxLine('-----------+----------------------+--------------+----------------');
for (const group of config.groups) {
const id = group.id.padEnd(10, ' ').substring(0, 10);
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
// Count UPS devices in this group
const upsInGroup = config.upsDevices.filter(ups => ups.groups.includes(group.id));
const upsCount = upsInGroup.length;
const upsNames = upsInGroup.map(ups => ups.name).join(', ');
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
}
}
logger.logBoxEnd();
} catch (error) {
logger.error(`Failed to list UPS groups: ${error.message}`);
}
}
/**
* Add a new UPS group
*/
public async add(): Promise<void> {
try {
// Import readline module for user input
const readline = await import('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 {
// Try to load configuration
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
// Initialize groups array if not exists
if (!config.groups) {
config.groups = [];
}
// Check if upsDevices is initialized
if (!config.upsDevices) {
config.upsDevices = [];
}
logger.log('\nNUPST Add Group');
logger.log('==============\n');
logger.log('This will guide you through creating a new UPS group.\n');
// Generate a new unique group ID
const groupId = helpers.shortId();
// Get group name
const name = await prompt('Group Name: ');
// Get group mode
const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: ');
const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
// Get optional description
const description = await prompt('Group Description (optional): ');
// Create the new group
const newGroup: IGroupConfig = {
id: groupId,
name: name || `Group-${groupId}`,
mode,
description: description || undefined
};
// Add the group to the configuration
config.groups.push(newGroup);
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
// Display summary
const boxWidth = 45;
logger.logBoxTitle('Group Created', boxWidth);
logger.logBoxLine(`ID: ${newGroup.id}`);
logger.logBoxLine(`Name: ${newGroup.name}`);
logger.logBoxLine(`Mode: ${newGroup.mode}`);
if (newGroup.description) {
logger.logBoxLine(`Description: ${newGroup.description}`);
}
logger.logBoxEnd();
// Check if there are UPS devices to assign to this group
if (config.upsDevices.length > 0) {
const assignUps = await prompt('Would you like to assign UPS devices to this group now? (y/N): ');
if (assignUps.toLowerCase() === 'y') {
await this.assignUpsToGroup(newGroup.id, config, prompt);
// Save again after assigning UPS devices
await this.nupst.getDaemon().saveConfig(config);
}
}
// Check if service is running and restart it if needed
this.nupst.getUpsHandler().restartServiceIfRunning();
logger.log('\nGroup setup complete!');
} finally {
rl.close();
}
} catch (error) {
logger.error(`Add group error: ${error.message}`);
}
}
/**
* Edit an existing UPS group
* @param groupId ID of the group to edit
*/
public async edit(groupId: string): Promise<void> {
try {
// Import readline module for user input
const readline = await import('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 {
// Try to load configuration
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
// Check if groups are initialized
if (!config.groups || !Array.isArray(config.groups)) {
logger.error('No groups configured. Please run "nupst group add" first to create a group.');
return;
}
// Find the group to edit
const groupIndex = config.groups.findIndex(group => group.id === groupId);
if (groupIndex === -1) {
logger.error(`Group with ID "${groupId}" not found.`);
return;
}
const group = config.groups[groupIndex];
logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`);
logger.log('==============================================\n');
// Edit group name
const newName = await prompt(`Group Name [${group.name}]: `);
if (newName.trim()) {
group.name = newName;
}
// Edit group mode
const currentMode = group.mode || 'redundant';
const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `);
if (modeInput.trim()) {
group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
}
// Edit description
const currentDesc = group.description || '';
const newDesc = await prompt(`Group Description [${currentDesc}]: `);
if (newDesc.trim() || newDesc === '') {
group.description = newDesc.trim() || undefined;
}
// Update the group in the configuration
config.groups[groupIndex] = group;
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
// Display summary
const boxWidth = 45;
logger.logBoxTitle('Group Updated', boxWidth);
logger.logBoxLine(`ID: ${group.id}`);
logger.logBoxLine(`Name: ${group.name}`);
logger.logBoxLine(`Mode: ${group.mode}`);
if (group.description) {
logger.logBoxLine(`Description: ${group.description}`);
}
logger.logBoxEnd();
// Edit UPS assignments if requested
const editAssignments = await prompt('Would you like to edit UPS assignments for this group? (y/N): ');
if (editAssignments.toLowerCase() === 'y') {
await this.assignUpsToGroup(group.id, config, prompt);
// Save again after editing assignments
await this.nupst.getDaemon().saveConfig(config);
}
// Check if service is running and restart it if needed
this.nupst.getUpsHandler().restartServiceIfRunning();
logger.log('\nGroup edit complete!');
} finally {
rl.close();
}
} catch (error) {
logger.error(`Edit group error: ${error.message}`);
}
}
/**
* Delete an existing UPS group
* @param groupId ID of the group to delete
*/
public async delete(groupId: string): Promise<void> {
try {
// Try to load configuration
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
// Check if groups are initialized
if (!config.groups || !Array.isArray(config.groups)) {
logger.error('No groups configured.');
return;
}
// Find the group to delete
const groupIndex = config.groups.findIndex(group => group.id === groupId);
if (groupIndex === -1) {
logger.error(`Group with ID "${groupId}" not found.`);
return;
}
const groupToDelete = config.groups[groupIndex];
// Get confirmation before deleting
const readline = await import('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 group "${groupToDelete.name}" (${groupId})? [y/N]: `, answer => {
resolve(answer.toLowerCase());
});
});
rl.close();
if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.');
return;
}
// Remove this group from all UPS device group assignments
if (config.upsDevices && Array.isArray(config.upsDevices)) {
for (const ups of config.upsDevices) {
const groupIndex = ups.groups.indexOf(groupId);
if (groupIndex !== -1) {
ups.groups.splice(groupIndex, 1);
}
}
}
// Remove the group from the array
config.groups.splice(groupIndex, 1);
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`);
// Check if service is running and restart it if needed
this.nupst.getUpsHandler().restartServiceIfRunning();
} catch (error) {
logger.error(`Failed to delete group: ${error.message}`);
}
}
/**
* Assign UPS devices to groups
* @param ups UPS configuration to update
* @param groups Available groups
* @param prompt Function to prompt for user input
*/
public async assignUpsToGroups(
ups: any,
groups: any[],
prompt: (question: string) => Promise<string>
): Promise<void> {
// Initialize groups array if it doesn't exist
if (!ups.groups) {
ups.groups = [];
}
// Show current group assignments
logger.log('\nCurrent Group Assignments:');
if (ups.groups && ups.groups.length > 0) {
for (const groupId of ups.groups) {
const group = groups.find(g => g.id === groupId);
if (group) {
logger.log(`- ${group.name} (${group.id})`);
} else {
logger.log(`- Unknown group (${groupId})`);
}
}
} else {
logger.log('- None');
}
// Show available groups
logger.log('\nAvailable Groups:');
if (groups.length === 0) {
logger.log('- No groups available. Use "nupst group add" to create groups.');
return;
}
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
const assigned = ups.groups && ups.groups.includes(group.id);
logger.log(`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
}
// Prompt for group selection
const selection = await prompt('\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ');
if (selection.toLowerCase() === 'clear') {
// Clear all group assignments
ups.groups = [];
logger.log('All group assignments cleared.');
return;
}
if (!selection.trim()) {
// No change if empty input
return;
}
// Process selections
const selections = selection.split(',').map(s => s.trim());
for (const sel of selections) {
const index = parseInt(sel, 10) - 1;
if (isNaN(index) || index < 0 || index >= groups.length) {
logger.error(`Invalid selection: ${sel}`);
continue;
}
const group = groups[index];
// Initialize groups array if needed (should already be done above)
if (!ups.groups) {
ups.groups = [];
}
// Toggle assignment
const groupIndex = ups.groups.indexOf(group.id);
if (groupIndex === -1) {
// Add to group
ups.groups.push(group.id);
logger.log(`Added to group: ${group.name} (${group.id})`);
} else {
// Remove from group
ups.groups.splice(groupIndex, 1);
logger.log(`Removed from group: ${group.name} (${group.id})`);
}
}
}
/**
* Assign UPS devices to a specific group
* @param groupId Group ID to assign UPS devices to
* @param config Full configuration
* @param prompt Function to prompt for user input
*/
public async assignUpsToGroup(
groupId: string,
config: any,
prompt: (question: string) => Promise<string>
): Promise<void> {
if (!config.upsDevices || config.upsDevices.length === 0) {
logger.log('No UPS devices available. Use "nupst add" to add UPS devices.');
return;
}
const group = config.groups.find(g => g.id === groupId);
if (!group) {
logger.error(`Group with ID "${groupId}" not found.`);
return;
}
// Show current assignments
logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`);
const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(groupId));
if (upsInGroup.length === 0) {
logger.log('- None');
} else {
for (const ups of upsInGroup) {
logger.log(`- ${ups.name} (${ups.id})`);
}
}
// Show all UPS devices
logger.log('\nAvailable UPS devices:');
for (let i = 0; i < config.upsDevices.length; i++) {
const ups = config.upsDevices[i];
const assigned = ups.groups && ups.groups.includes(groupId);
logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
}
// Prompt for UPS selection
const selection = await prompt('\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ');
if (selection.toLowerCase() === 'clear') {
// Clear all UPS from this group
for (const ups of config.upsDevices) {
if (ups.groups) {
const groupIndex = ups.groups.indexOf(groupId);
if (groupIndex !== -1) {
ups.groups.splice(groupIndex, 1);
}
}
}
logger.log(`All UPS devices removed from group "${group.name}".`);
return;
}
if (!selection.trim()) {
// No change if empty input
return;
}
// Process selections
const selections = selection.split(',').map(s => s.trim());
for (const sel of selections) {
const index = parseInt(sel, 10) - 1;
if (isNaN(index) || index < 0 || index >= config.upsDevices.length) {
logger.error(`Invalid selection: ${sel}`);
continue;
}
const ups = config.upsDevices[index];
// Initialize groups array if needed
if (!ups.groups) {
ups.groups = [];
}
// Toggle assignment
const groupIndex = ups.groups.indexOf(groupId);
if (groupIndex === -1) {
// Add to group
ups.groups.push(groupId);
logger.log(`Added "${ups.name}" to group "${group.name}"`);
} else {
// Remove from group
ups.groups.splice(groupIndex, 1);
logger.log(`Removed "${ups.name}" from group "${group.name}"`);
}
}
}
}

320
ts/cli/service-handler.ts Normal file
View File

@ -0,0 +1,320 @@
import { execSync } from 'child_process';
import { Nupst } from '../nupst.js';
import { logger } from '../logger.js';
/**
* Class for handling service-related CLI commands
* Provides interface for managing systemd service
*/
export class ServiceHandler {
private readonly nupst: Nupst;
/**
* Create a new Service handler
* @param nupst Reference to the main Nupst instance
*/
constructor(nupst: Nupst) {
this.nupst = nupst;
}
/**
* Enable the service (requires root)
*/
public async enable(): Promise<void> {
this.checkRootAccess('This command must be run as root.');
await this.nupst.getSystemd().install();
logger.log('NUPST service has been installed. Use "nupst start" to start the service.');
}
/**
* Start the daemon directly
* @param debugMode Whether to enable debug mode
*/
public async daemonStart(debugMode: boolean = false): Promise<void> {
logger.log('Starting NUPST daemon...');
try {
// Enable debug mode for SNMP if requested
if (debugMode) {
this.nupst.getSnmp().enableDebug();
logger.log('SNMP debug mode enabled');
}
await this.nupst.getDaemon().start();
} catch (error) {
// Error is already logged and process.exit is called in daemon.start()
// No need to handle it here
}
}
/**
* Show logs of the systemd service
*/
public async logs(): Promise<void> {
try {
// Use exec with spawn to properly follow logs in real-time
const { spawn } = await import('child_process');
logger.log('Tailing nupst service logs (Ctrl+C to exit)...\n');
const journalctl = spawn('journalctl', ['-u', 'nupst.service', '-n', '50', '-f'], {
stdio: ['ignore', 'inherit', 'inherit'],
});
// Forward signals to child process
process.on('SIGINT', () => {
journalctl.kill('SIGINT');
process.exit(0);
});
// Wait for process to exit
await new Promise<void>((resolve) => {
journalctl.on('exit', () => resolve());
});
} catch (error) {
logger.error(`Failed to retrieve logs: ${error}`);
process.exit(1);
}
}
/**
* Stop the systemd service
*/
public async stop(): Promise<void> {
await this.nupst.getSystemd().stop();
}
/**
* Start the systemd service
*/
public async start(): Promise<void> {
try {
await this.nupst.getSystemd().start();
} catch (error) {
// Error will be displayed by systemd.start()
process.exit(1);
}
}
/**
* Show status of the systemd service and UPS
*/
public async status(): Promise<void> {
// Extract debug options from args array
const debugOptions = this.extractDebugOptions(process.argv);
await this.nupst.getSystemd().getStatus(debugOptions.debugMode);
}
/**
* Disable the service (requires root)
*/
public async disable(): Promise<void> {
this.checkRootAccess('This command must be run as root.');
await this.nupst.getSystemd().disable();
}
/**
* Check if the user has root access
* @param errorMessage Error message to display if not root
*/
private checkRootAccess(errorMessage: string): void {
if (process.getuid && process.getuid() !== 0) {
logger.error(errorMessage);
process.exit(1);
}
}
/**
* Update NUPST from repository and refresh systemd service
*/
public async update(): Promise<void> {
try {
// Check if running as root
this.checkRootAccess(
'This command must be run as root to update NUPST and refresh the systemd service.'
);
const boxWidth = 45;
logger.logBoxTitle('NUPST Update Process', boxWidth);
logger.logBoxLine('Updating NUPST from repository...');
// Determine the installation directory (assuming it's either /opt/nupst or the current directory)
const { existsSync } = await import('fs');
let installDir = '/opt/nupst';
if (!existsSync(installDir)) {
// If not installed in /opt/nupst, use the current directory
const { dirname } = await import('path');
installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable
logger.logBoxLine(`Using local installation directory: ${installDir}`);
}
try {
// 1. Update the repository
logger.logBoxLine('Pulling latest changes from git repository...');
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, {
stdio: 'pipe',
});
// 2. Run the install.sh script
logger.logBoxLine('Running install.sh to update NUPST...');
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
// 3. Run the setup.sh script with force flag to update Node.js and dependencies
logger.logBoxLine('Running setup.sh to update Node.js and dependencies...');
execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' });
// 4. Refresh the systemd service
logger.logBoxLine('Refreshing systemd service...');
// First check if service exists
let serviceExists = false;
try {
const output = execSync('systemctl list-unit-files | grep nupst.service').toString();
serviceExists = output.includes('nupst.service');
} catch (error) {
// If grep fails (service not found), serviceExists remains false
serviceExists = false;
}
if (serviceExists) {
// Stop the service if it's running
const isRunning =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isRunning) {
logger.logBoxLine('Stopping nupst service...');
execSync('systemctl stop nupst.service');
}
// Reinstall the service
logger.logBoxLine('Reinstalling systemd service...');
await this.nupst.getSystemd().install();
// Restart the service if it was running
if (isRunning) {
logger.logBoxLine('Restarting nupst service...');
execSync('systemctl start nupst.service');
}
} else {
logger.logBoxLine('Systemd service not installed, skipping service refresh.');
logger.logBoxLine('Run "nupst enable" to install the service.');
}
logger.logBoxLine('Update completed successfully!');
logger.logBoxEnd();
} catch (error) {
logger.logBoxLine('Error during update process:');
logger.logBoxLine(`${error.message}`);
logger.logBoxEnd();
process.exit(1);
}
} catch (error) {
logger.error(`Update failed: ${error.message}`);
process.exit(1);
}
}
/**
* Completely uninstall NUPST from the system
*/
public async uninstall(): Promise<void> {
// Check if running as root
this.checkRootAccess('This command must be run as root.');
try {
// Import readline module for user input
const readline = await import('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);
});
});
};
console.log('\nNUPST Uninstaller');
console.log('===============');
console.log('This will completely remove NUPST from your system.\n');
// Ask about removing configuration
const removeConfig = await prompt(
'Do you want to remove the NUPST configuration files? (y/N): '
);
// Find the uninstall.sh script location
let uninstallScriptPath: string;
// Try to determine script location based on executable path
try {
// For ESM, we can use import.meta.url, but since we might be in CJS
// we'll use a more reliable approach based on process.argv[1]
const binPath = process.argv[1];
const { dirname, join } = await import('path');
const modulePath = dirname(dirname(binPath));
uninstallScriptPath = join(modulePath, 'uninstall.sh');
// Check if the script exists
const { access } = await import('fs/promises');
await access(uninstallScriptPath);
} catch (error) {
// If we can't find it in the expected location, try common installation paths
const commonPaths = ['/opt/nupst/uninstall.sh', `${process.cwd()}/uninstall.sh`];
const { existsSync } = await import('fs');
uninstallScriptPath = '';
for (const path of commonPaths) {
if (existsSync(path)) {
uninstallScriptPath = path;
break;
}
}
if (!uninstallScriptPath) {
console.error('Could not locate uninstall.sh script. Aborting uninstall.');
rl.close();
process.exit(1);
}
}
// Close readline before executing script
rl.close();
// Execute uninstall.sh with the appropriate option
console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`);
// Pass the configuration removal option as an environment variable
const env = {
...process.env,
REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no',
REMOVE_REPO: 'yes', // Always remove repo as requested
NUPST_CLI_CALL: 'true', // Flag to indicate this is being called from CLI
};
// Run the uninstall script with sudo
execSync(`sudo bash ${uninstallScriptPath}`, {
env,
stdio: 'inherit', // Show output in the terminal
});
} catch (error) {
console.error(`Uninstall failed: ${error.message}`);
process.exit(1);
}
}
/**
* Extract and remove debug options from args array
* @param args Command line arguments
* @returns Object with debug flags and cleaned args
*/
private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } {
const debugMode = args.includes('--debug') || args.includes('-d');
// Remove debug flags from args
const cleanedArgs = args.filter((arg) => arg !== '--debug' && arg !== '-d');
return { debugMode, cleanedArgs };
}
}

986
ts/cli/ups-handler.ts Normal file
View File

@ -0,0 +1,986 @@
import { execSync } from 'child_process';
import { Nupst } from '../nupst.js';
import { logger } from '../logger.js';
import * as helpers from '../helpers/index.js';
/**
* 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('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();
}
} catch (error) {
logger.error(`Add UPS error: ${error.message}`);
}
}
/**
* 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'
},
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);
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('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();
}
} catch (error) {
logger.error(`Edit UPS error: ${error.message}`);
}
}
/**
* 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
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 delete(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('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();
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.message}`);
}
}
/**
* 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.');
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.message}`);
}
}
/**
* 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.message}`);
}
}
/**
* 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.message}`);
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;
console.log('\nSNMP Version:');
console.log(' 1) SNMPv1');
console.log(' 2) SNMPv2c');
console.log(' 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> {
console.log('\nSNMPv3 Security Settings:');
// Security Level
console.log('\nSecurity Level:');
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)');
console.log(' 2) authNoPriv (Authentication, No Privacy)');
console.log(' 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
console.log(
'\nSNMPv3 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
console.log('\nAuthentication Protocol:');
console.log(' 1) MD5');
console.log(' 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
console.log('\nPrivacy Protocol:');
console.log(' 1) DES');
console.log(' 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> {
console.log('\nShutdown 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> {
console.log('\nUPS Model Selection:');
console.log(' 1) CyberPower');
console.log(' 2) APC');
console.log(' 3) Eaton');
console.log(' 4) TrippLite');
console.log(' 5) Liebert/Vertiv');
console.log(' 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';
console.log('\nEnter custom OIDs for your UPS:');
console.log('(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.message}`);
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 async restartServiceIfRunning(): Promise<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.message}`);
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
}
}
}

View File

@ -2,9 +2,11 @@ import { NupstSnmp } from './snmp/manager.js';
import { NupstDaemon } from './daemon.js';
import { NupstSystemd } from './systemd.js';
import { commitinfo } from './00_commitinfo_data.js';
import { spawn } from 'child_process';
import * as https from 'https';
import { logger } from './logger.js';
import { UpsHandler } from './cli/ups-handler.js';
import { GroupHandler } from './cli/group-handler.js';
import { ServiceHandler } from './cli/service-handler.js';
import * as https from 'https';
/**
* Main Nupst class that coordinates all components
@ -14,6 +16,9 @@ export class Nupst {
private readonly snmp: NupstSnmp;
private readonly daemon: NupstDaemon;
private readonly systemd: NupstSystemd;
private readonly upsHandler: UpsHandler;
private readonly groupHandler: GroupHandler;
private readonly serviceHandler: ServiceHandler;
private updateAvailable: boolean = false;
private latestVersion: string = '';
@ -21,10 +26,16 @@ export class Nupst {
* Create a new Nupst instance with all necessary components
*/
constructor() {
// Initialize core components
this.snmp = new NupstSnmp();
this.snmp.setNupst(this); // Set up bidirectional reference
this.daemon = new NupstDaemon(this.snmp);
this.systemd = new NupstSystemd(this.daemon);
// Initialize handlers
this.upsHandler = new UpsHandler(this);
this.groupHandler = new GroupHandler(this);
this.serviceHandler = new ServiceHandler(this);
}
/**
@ -48,6 +59,27 @@ export class Nupst {
return this.systemd;
}
/**
* Get the UPS handler for UPS management
*/
public getUpsHandler(): UpsHandler {
return this.upsHandler;
}
/**
* Get the Group handler for group management
*/
public getGroupHandler(): GroupHandler {
return this.groupHandler;
}
/**
* Get the Service handler for service management
*/
public getServiceHandler(): ServiceHandler {
return this.serviceHandler;
}
/**
* Get the current version of NUPST
* @returns The current version string
@ -192,4 +224,4 @@ export class Nupst {
logger.logBoxEnd();
}
}
}
}