Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
d14ba1dd65 | |||
7d595fa175 | |||
df417432b0 | |||
e5f1ebf343 | |||
3ff0dd7ac8 | |||
bb87316dd3 | |||
d6e0a1a274 | |||
95fa4f8b0b | |||
c2f2f1e2ee | |||
936f86c346 | |||
7ff1a7da36 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "4.0.5",
|
||||
"version": "4.1.4",
|
||||
"exports": "./mod.ts",
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-all mod.ts",
|
||||
|
@@ -6,5 +6,5 @@ import denoConfig from '../deno.json' with { type: 'json' };
|
||||
export const commitinfo = {
|
||||
name: denoConfig.name,
|
||||
version: denoConfig.version,
|
||||
description: 'Deno-powered UPS monitoring tool for SNMP-enabled UPS devices',
|
||||
description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)',
|
||||
};
|
||||
|
@@ -469,7 +469,7 @@ export class NupstCli {
|
||||
private showVersion(): void {
|
||||
const version = this.nupst.getVersion();
|
||||
logger.log(`NUPST version ${version}`);
|
||||
logger.log('Deno-powered UPS monitoring tool');
|
||||
logger.log('Network UPS Shutdown Tool (https://nupst.serve.zone)');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -192,6 +192,7 @@ export class GroupHandler {
|
||||
logger.log('\nGroup setup complete!');
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
@@ -309,6 +310,7 @@ export class GroupHandler {
|
||||
logger.log('\nGroup edit complete!');
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
@@ -366,6 +368,7 @@ export class GroupHandler {
|
||||
});
|
||||
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
|
||||
if (confirm !== 'y' && confirm !== 'yes') {
|
||||
logger.log('Deletion cancelled.');
|
||||
|
@@ -129,81 +129,57 @@ export class ServiceHandler {
|
||||
try {
|
||||
// Check if running as root
|
||||
this.checkRootAccess(
|
||||
'This command must be run as root to update NUPST and refresh the systemd service.',
|
||||
'This command must be run as root to update NUPST.',
|
||||
);
|
||||
|
||||
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}`);
|
||||
}
|
||||
console.log('');
|
||||
logger.info('Checking for updates...');
|
||||
|
||||
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',
|
||||
// Get current version
|
||||
const currentVersion = this.nupst.getVersion();
|
||||
|
||||
// Fetch latest version from Gitea API
|
||||
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest';
|
||||
const response = execSync(`curl -sSL ${apiUrl}`).toString();
|
||||
const release = JSON.parse(response);
|
||||
const latestVersion = release.tag_name; // e.g., "v4.0.7"
|
||||
|
||||
// Normalize versions for comparison (ensure both have "v" prefix)
|
||||
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
|
||||
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
|
||||
|
||||
logger.dim(`Current version: ${normalizedCurrent}`);
|
||||
logger.dim(`Latest version: ${normalizedLatest}`);
|
||||
console.log('');
|
||||
|
||||
// Compare normalized versions
|
||||
if (normalizedCurrent === normalizedLatest) {
|
||||
logger.success('Already up to date!');
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`New version available: ${latestVersion}`);
|
||||
logger.dim('Downloading and installing...');
|
||||
console.log('');
|
||||
|
||||
// Download and run the install script
|
||||
// This handles everything: download binary, stop service, replace, restart
|
||||
const installUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh';
|
||||
|
||||
execSync(`curl -sSL ${installUrl} | bash`, {
|
||||
stdio: 'inherit', // Show install script output to user
|
||||
});
|
||||
|
||||
// 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();
|
||||
console.log('');
|
||||
logger.success(`Updated to ${latestVersion}`);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
logger.logBoxLine('Error during update process:');
|
||||
logger.logBoxLine(`${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.logBoxEnd();
|
||||
console.log('');
|
||||
logger.error('Update failed');
|
||||
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -237,9 +213,11 @@ export class ServiceHandler {
|
||||
});
|
||||
};
|
||||
|
||||
console.log('\nNUPST Uninstaller');
|
||||
console.log('===============');
|
||||
console.log('This will completely remove NUPST from your system.\n');
|
||||
logger.log('');
|
||||
logger.highlight('NUPST Uninstaller');
|
||||
logger.dim('===============');
|
||||
logger.log('This will completely remove NUPST from your system.');
|
||||
logger.log('');
|
||||
|
||||
// Ask about removing configuration
|
||||
const removeConfig = await prompt(
|
||||
@@ -275,17 +253,20 @@ export class ServiceHandler {
|
||||
}
|
||||
|
||||
if (!uninstallScriptPath) {
|
||||
console.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
||||
logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Close readline before executing script
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
|
||||
// Execute uninstall.sh with the appropriate option
|
||||
console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`);
|
||||
logger.log('');
|
||||
logger.log(`Running uninstaller from ${uninstallScriptPath}...`);
|
||||
|
||||
// Pass the configuration removal option as an environment variable
|
||||
const env = {
|
||||
@@ -301,7 +282,7 @@ export class ServiceHandler {
|
||||
stdio: 'inherit', // Show output in the terminal
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
@@ -47,6 +47,7 @@ export class UpsHandler {
|
||||
await this.runAddProcess(prompt);
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
@@ -178,6 +179,7 @@ export class UpsHandler {
|
||||
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)}`);
|
||||
@@ -344,6 +346,7 @@ export class UpsHandler {
|
||||
});
|
||||
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
|
||||
if (confirm !== 'y' && confirm !== 'yes') {
|
||||
logger.log('Deletion cancelled.');
|
||||
@@ -667,10 +670,11 @@ export class UpsHandler {
|
||||
|
||||
// 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)');
|
||||
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)
|
||||
@@ -697,13 +701,15 @@ export class UpsHandler {
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
console.log('\nSNMPv3 Security Settings:');
|
||||
logger.log('');
|
||||
logger.info('SNMPv3 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)');
|
||||
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
|
||||
@@ -752,8 +758,9 @@ export class UpsHandler {
|
||||
|
||||
// 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.',
|
||||
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);
|
||||
@@ -773,9 +780,10 @@ export class UpsHandler {
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Authentication protocol
|
||||
console.log('\nAuthentication Protocol:');
|
||||
console.log(' 1) MD5');
|
||||
console.log(' 2) SHA');
|
||||
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}]: `,
|
||||
@@ -799,9 +807,10 @@ export class UpsHandler {
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Privacy protocol
|
||||
console.log('\nPrivacy Protocol:');
|
||||
console.log(' 1) DES');
|
||||
console.log(' 2) AES');
|
||||
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;
|
||||
@@ -822,7 +831,8 @@ export class UpsHandler {
|
||||
thresholds: any,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
console.log('\nShutdown Thresholds:');
|
||||
logger.log('');
|
||||
logger.info('Shutdown Thresholds:');
|
||||
|
||||
// Battery threshold
|
||||
const defaultBatteryThreshold = thresholds.battery || 60;
|
||||
@@ -854,13 +864,14 @@ export class UpsHandler {
|
||||
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)');
|
||||
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
|
||||
@@ -891,8 +902,9 @@ export class UpsHandler {
|
||||
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)');
|
||||
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: ');
|
||||
|
11
ts/daemon.ts
11
ts/daemon.ts
@@ -207,11 +207,9 @@ export class NupstDaemon {
|
||||
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2));
|
||||
this.config = configToSave;
|
||||
|
||||
console.log('┌─ Configuration Saved ─────────────────────┐');
|
||||
console.log(`│ Location: ${this.CONFIG_PATH}`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
logger.logBox('Configuration Saved', [`Location: ${this.CONFIG_PATH}`], 45, 'success');
|
||||
} catch (error) {
|
||||
console.error('Error saving configuration:', error);
|
||||
logger.error(`Error saving configuration: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,10 +217,7 @@ export class NupstDaemon {
|
||||
* Helper method to log configuration errors consistently
|
||||
*/
|
||||
private logConfigError(message: string): void {
|
||||
console.error('┌─ Configuration Error ─────────────────────┐');
|
||||
console.error(`│ ${message}`);
|
||||
console.error("│ Please run 'nupst setup' first to create a configuration.");
|
||||
console.error('└───────────────────────────────────────────┘');
|
||||
logger.logBox('Configuration Error', [message, "Please run 'nupst setup' first to create a configuration."], 45, 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -34,12 +34,14 @@ export class MigrationRunner {
|
||||
let currentConfig = config;
|
||||
let anyMigrationsRan = false;
|
||||
|
||||
logger.dim('Checking for required config migrations...');
|
||||
|
||||
for (const migration of this.migrations) {
|
||||
const shouldRun = await migration.shouldRun(currentConfig);
|
||||
|
||||
if (shouldRun) {
|
||||
// Only show "checking" message when we actually need to migrate
|
||||
if (!anyMigrationsRan) {
|
||||
logger.dim('Checking for required config migrations...');
|
||||
}
|
||||
logger.info(`Running ${migration.getName()}...`);
|
||||
currentConfig = await migration.migrate(currentConfig);
|
||||
anyMigrationsRan = true;
|
||||
@@ -49,7 +51,7 @@ export class MigrationRunner {
|
||||
if (anyMigrationsRan) {
|
||||
logger.success('Configuration migrations complete');
|
||||
} else {
|
||||
logger.dim('No migrations needed');
|
||||
logger.success('config format ok');
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -525,6 +525,7 @@ export class NupstSnmp {
|
||||
|
||||
/**
|
||||
* Determine power status based on UPS model and raw value
|
||||
* Uses the value mappings defined in the OID sets
|
||||
* @param upsModel UPS model
|
||||
* @param powerStatusValue Raw power status value
|
||||
* @returns Standardized power status
|
||||
@@ -533,39 +534,28 @@ export class NupstSnmp {
|
||||
upsModel: TUpsModel | undefined,
|
||||
powerStatusValue: number,
|
||||
): 'online' | 'onBattery' | 'unknown' {
|
||||
if (upsModel === 'cyberpower') {
|
||||
// CyberPower RMCARD205: upsBaseOutputStatus values
|
||||
// 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc.
|
||||
if (powerStatusValue === 2) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 3) {
|
||||
return 'onBattery';
|
||||
}
|
||||
} else if (upsModel === 'eaton') {
|
||||
// Eaton UPS: xupsOutputSource values
|
||||
// 3=normal/mains, 5=battery, etc.
|
||||
if (powerStatusValue === 3) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 5) {
|
||||
return 'onBattery';
|
||||
}
|
||||
} else if (upsModel === 'apc') {
|
||||
// APC UPS: upsBasicOutputStatus values
|
||||
// 2=online, 3=onBattery, etc.
|
||||
if (powerStatusValue === 2) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 3) {
|
||||
return 'onBattery';
|
||||
}
|
||||
} else {
|
||||
// Default interpretation for other UPS models
|
||||
if (powerStatusValue === 1) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 2) {
|
||||
return 'onBattery';
|
||||
// Get the OID set for this UPS model
|
||||
if (upsModel && upsModel !== 'custom') {
|
||||
const oidSet = UpsOidSets.getOidSet(upsModel);
|
||||
|
||||
// Use the value mappings if available
|
||||
if (oidSet.POWER_STATUS_VALUES) {
|
||||
if (powerStatusValue === oidSet.POWER_STATUS_VALUES.online) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === oidSet.POWER_STATUS_VALUES.onBattery) {
|
||||
return 'onBattery';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for custom or undefined models (RFC 1628 standard)
|
||||
// upsOutputSource: 3=normal (mains), 5=battery
|
||||
if (powerStatusValue === 3) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 5) {
|
||||
return 'onBattery';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
|
@@ -11,37 +11,57 @@ export class UpsOidSets {
|
||||
private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = {
|
||||
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
|
||||
cyberpower: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus (2=online, 3=on battery)
|
||||
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 2, // upsBaseOutputStatus: 2=onLine
|
||||
onBattery: 3, // upsBaseOutputStatus: 3=onBattery
|
||||
},
|
||||
},
|
||||
|
||||
// APC OIDs
|
||||
apc: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery)
|
||||
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // upsBasicOutputStatus
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 2, // upsBasicOutputStatus: 2=onLine
|
||||
onBattery: 3, // upsBasicOutputStatus: 3=onBattery
|
||||
},
|
||||
},
|
||||
|
||||
// Eaton OIDs
|
||||
eaton: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery)
|
||||
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 3, // xupsOutputSource: 3=normal (mains power)
|
||||
onBattery: 5, // xupsOutputSource: 5=battery
|
||||
},
|
||||
},
|
||||
|
||||
// TrippLite OIDs
|
||||
tripplite: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status
|
||||
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // tlUpsOutputSource
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 2, // tlUpsOutputSource: 2=normal (mains power)
|
||||
onBattery: 3, // tlUpsOutputSource: 3=onBattery
|
||||
},
|
||||
},
|
||||
|
||||
// Liebert/Vertiv OIDs
|
||||
liebert: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status
|
||||
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // lgpPwrOutputSource
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 2, // lgpPwrOutputSource: 2=normal (mains power)
|
||||
onBattery: 3, // lgpPwrOutputSource: 3=onBattery
|
||||
},
|
||||
},
|
||||
|
||||
// Custom OIDs (to be provided by the user)
|
||||
|
@@ -28,6 +28,13 @@ export interface IOidSet {
|
||||
BATTERY_CAPACITY: string;
|
||||
/** OID for battery runtime */
|
||||
BATTERY_RUNTIME: string;
|
||||
/** Power status value mappings */
|
||||
POWER_STATUS_VALUES?: {
|
||||
/** SNMP value that indicates UPS is online (on AC power) */
|
||||
online: number;
|
||||
/** SNMP value that indicates UPS is on battery */
|
||||
onBattery: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
210
ts/systemd.ts
210
ts/systemd.ts
@@ -3,6 +3,7 @@ import { promises as fs } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { NupstDaemon } from './daemon.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||
|
||||
/**
|
||||
* Class for managing systemd service
|
||||
@@ -49,11 +50,11 @@ WantedBy=multi-user.target
|
||||
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();
|
||||
logger.log('');
|
||||
logger.error('No configuration found');
|
||||
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
|
||||
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
|
||||
logger.log('');
|
||||
throw new Error('Configuration not found');
|
||||
}
|
||||
}
|
||||
@@ -133,21 +134,59 @@ WantedBy=multi-user.target
|
||||
* Get status of the systemd service and UPS
|
||||
* @param debugMode Whether to enable debug mode for SNMP
|
||||
*/
|
||||
/**
|
||||
* Display version information and update status
|
||||
* @private
|
||||
*/
|
||||
private async displayVersionInfo(): Promise<void> {
|
||||
try {
|
||||
const nupst = this.daemon.getNupstSnmp().getNupst();
|
||||
const version = nupst.getVersion();
|
||||
|
||||
// Check for updates
|
||||
const updateAvailable = await nupst.checkForUpdates();
|
||||
|
||||
// Display version info
|
||||
if (updateAvailable) {
|
||||
const updateStatus = nupst.getUpdateStatus();
|
||||
logger.log('');
|
||||
logger.log(
|
||||
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)}`,
|
||||
);
|
||||
logger.log(` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`);
|
||||
} else {
|
||||
logger.log('');
|
||||
logger.log(
|
||||
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${theme.success('Up to date')}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// If version check fails, show at least the current version
|
||||
try {
|
||||
const nupst = this.daemon.getNupstSnmp().getNupst();
|
||||
const version = nupst.getVersion();
|
||||
logger.log('');
|
||||
logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`);
|
||||
} catch (_innerError) {
|
||||
// Silently fail if we can't even get the version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
console.log('');
|
||||
logger.info('Debug Mode: SNMP debugging enabled');
|
||||
console.log('');
|
||||
this.daemon.getNupstSnmp().enableDebug();
|
||||
}
|
||||
|
||||
// Display version information
|
||||
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
|
||||
// Display version and update status first
|
||||
await this.displayVersionInfo();
|
||||
|
||||
// Check if config exists first
|
||||
// Check if config exists
|
||||
try {
|
||||
await this.checkConfigExists();
|
||||
} catch (error) {
|
||||
@@ -171,18 +210,50 @@ WantedBy=multi-user.target
|
||||
private displayServiceStatus(): 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();
|
||||
const lines = serviceStatus.split('\n');
|
||||
|
||||
// Parse key information from systemctl output
|
||||
let isActive = false;
|
||||
let pid = '';
|
||||
let memory = '';
|
||||
let cpu = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('Active:')) {
|
||||
isActive = line.includes('active (running)');
|
||||
} else if (line.includes('Main PID:')) {
|
||||
const match = line.match(/Main PID:\s+(\d+)/);
|
||||
if (match) pid = match[1];
|
||||
} else if (line.includes('Memory:')) {
|
||||
const match = line.match(/Memory:\s+([\d.]+[A-Z])/);
|
||||
if (match) memory = match[1];
|
||||
} else if (line.includes('CPU:')) {
|
||||
const match = line.match(/CPU:\s+([\d.]+(?:ms|s))/);
|
||||
if (match) cpu = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Display beautiful status
|
||||
logger.log('');
|
||||
if (isActive) {
|
||||
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
|
||||
} else {
|
||||
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
|
||||
}
|
||||
|
||||
if (pid || memory || cpu) {
|
||||
const details = [];
|
||||
if (pid) details.push(`PID: ${theme.dim(pid)}`);
|
||||
if (memory) details.push(`Memory: ${theme.dim(memory)}`);
|
||||
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
|
||||
logger.log(` ${details.join(' ')}`);
|
||||
}
|
||||
logger.log('');
|
||||
|
||||
} catch (error) {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
logger.logBoxLine('Service is not running');
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
|
||||
logger.log('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +270,7 @@ WantedBy=multi-user.target
|
||||
|
||||
// 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`);
|
||||
logger.info(`UPS Devices (${config.upsDevices.length}):`);
|
||||
|
||||
// Show status for each UPS
|
||||
for (const ups of config.upsDevices) {
|
||||
@@ -207,6 +278,7 @@ WantedBy=multi-user.target
|
||||
}
|
||||
} else if (config.snmp) {
|
||||
// Legacy single UPS configuration
|
||||
logger.info('UPS Devices (1):');
|
||||
const legacyUps = {
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
@@ -217,15 +289,16 @@ WantedBy=multi-user.target
|
||||
|
||||
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||
} else {
|
||||
logger.error('No UPS devices found in configuration');
|
||||
logger.log('');
|
||||
logger.warn('No UPS devices configured');
|
||||
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
|
||||
logger.log('');
|
||||
}
|
||||
} catch (error) {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('UPS Status', boxWidth);
|
||||
logger.logBoxLine(
|
||||
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
logger.error('Failed to retrieve UPS status');
|
||||
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||
logger.log('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,24 +308,6 @@ WantedBy=multi-user.target
|
||||
* @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: string) => {
|
||||
const group = config.groups?.find((g: { id: string }) => 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 = {
|
||||
@@ -262,32 +317,43 @@ WantedBy=multi-user.target
|
||||
|
||||
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`);
|
||||
// Determine status symbol based on power status
|
||||
let statusSymbol = symbols.unknown;
|
||||
if (status.powerStatus === 'online') {
|
||||
statusSymbol = symbols.running;
|
||||
} else if (status.powerStatus === 'onBattery') {
|
||||
statusSymbol = symbols.warning;
|
||||
}
|
||||
|
||||
// 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 ? '⚠️' : '✓'
|
||||
}`,
|
||||
);
|
||||
// Display UPS name and power status
|
||||
logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
|
||||
|
||||
// Display battery with color coding
|
||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
|
||||
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
||||
|
||||
// Display host info
|
||||
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||
|
||||
// Display groups if any
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
const config = this.daemon.getConfig();
|
||||
const groupNames = ups.groups.map((groupId: string) => {
|
||||
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
|
||||
return group ? group.name : groupId;
|
||||
});
|
||||
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
|
||||
}
|
||||
|
||||
logger.log('');
|
||||
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(
|
||||
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
logger.logBoxEnd();
|
||||
// Display error for this UPS
|
||||
logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
|
||||
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||
logger.log('');
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user