Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
a7113d0387 | |||
61d4e9037a | |||
caced2718f | |||
8516056f84 | |||
07ec9d7595 | |||
d14ba1dd65 | |||
7d595fa175 | |||
df417432b0 | |||
e5f1ebf343 | |||
3ff0dd7ac8 | |||
bb87316dd3 | |||
d6e0a1a274 | |||
95fa4f8b0b | |||
c2f2f1e2ee | |||
936f86c346 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/nupst",
|
"name": "@serve.zone/nupst",
|
||||||
"version": "4.0.6",
|
"version": "4.1.6",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --allow-all mod.ts",
|
"dev": "deno run --allow-all mod.ts",
|
||||||
|
@@ -6,5 +6,5 @@ import denoConfig from '../deno.json' with { type: 'json' };
|
|||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: denoConfig.name,
|
name: denoConfig.name,
|
||||||
version: denoConfig.version,
|
version: denoConfig.version,
|
||||||
description: 'Deno-powered UPS monitoring tool for SNMP-enabled UPS devices',
|
description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)',
|
||||||
};
|
};
|
||||||
|
254
ts/cli.ts
254
ts/cli.ts
@@ -1,6 +1,6 @@
|
|||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { Nupst } from './nupst.ts';
|
import { Nupst } from './nupst.ts';
|
||||||
import { logger } from './logger.ts';
|
import { logger, type ITableColumn } from './logger.ts';
|
||||||
import { theme, symbols } from './colors.ts';
|
import { theme, symbols } from './colors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -303,154 +303,164 @@ export class NupstCli {
|
|||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
const errorBoxWidth = 45;
|
logger.logBox('Configuration Error', [
|
||||||
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
'No configuration found.',
|
||||||
logger.logBoxLine('No configuration found.');
|
"Please run 'nupst ups add' first to create a configuration.",
|
||||||
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
], 50, 'error');
|
||||||
logger.logBoxEnd();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current configuration
|
// Get current configuration
|
||||||
const config = this.nupst.getDaemon().getConfig();
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
const boxWidth = 50;
|
|
||||||
logger.logBoxTitle('NUPST Configuration', boxWidth);
|
|
||||||
|
|
||||||
// Check if multi-UPS config
|
// Check if multi-UPS config
|
||||||
if (config.upsDevices && Array.isArray(config.upsDevices)) {
|
if (config.upsDevices && Array.isArray(config.upsDevices)) {
|
||||||
// Multi-UPS configuration
|
// === Multi-UPS Configuration ===
|
||||||
logger.logBoxLine(`UPS Devices: ${config.upsDevices.length}`);
|
|
||||||
logger.logBoxLine(`Groups: ${config.groups ? config.groups.length : 0}`);
|
// Overview Box
|
||||||
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
|
logger.log('');
|
||||||
logger.logBoxLine('');
|
logger.logBox('NUPST Configuration', [
|
||||||
logger.logBoxLine('Configuration File Location:');
|
`UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`,
|
||||||
logger.logBoxLine(' /etc/nupst/config.json');
|
`Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`,
|
||||||
logger.logBoxEnd();
|
`Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
|
||||||
|
'',
|
||||||
|
theme.dim('Configuration File:'),
|
||||||
|
` ${theme.path('/etc/nupst/config.json')}`,
|
||||||
|
], 60, 'info');
|
||||||
|
|
||||||
// Show UPS devices
|
// UPS Devices Table
|
||||||
if (config.upsDevices.length > 0) {
|
if (config.upsDevices.length > 0) {
|
||||||
logger.logBoxTitle('UPS Devices', boxWidth);
|
const upsRows = config.upsDevices.map((ups) => ({
|
||||||
for (const ups of config.upsDevices) {
|
name: ups.name,
|
||||||
logger.logBoxLine(`${ups.name} (${ups.id}):`);
|
id: theme.dim(ups.id),
|
||||||
logger.logBoxLine(` Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
host: `${ups.snmp.host}:${ups.snmp.port}`,
|
||||||
logger.logBoxLine(` Model: ${ups.snmp.upsModel}`);
|
model: ups.snmp.upsModel || 'cyberpower',
|
||||||
logger.logBoxLine(
|
thresholds: `${ups.thresholds.battery}% / ${ups.thresholds.runtime}min`,
|
||||||
` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`,
|
groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
|
||||||
);
|
}));
|
||||||
logger.logBoxLine(
|
|
||||||
` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`,
|
const upsColumns: ITableColumn[] = [
|
||||||
);
|
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
logger.logBoxLine('');
|
{ header: 'ID', key: 'id', align: 'left' },
|
||||||
}
|
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
||||||
logger.logBoxEnd();
|
{ header: 'Model', key: 'model', align: 'left' },
|
||||||
|
{ header: 'Battery/Runtime', key: 'thresholds', align: 'left' },
|
||||||
|
{ header: 'Groups', key: 'groups', align: 'left' },
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.info(`UPS Devices (${config.upsDevices.length}):`);
|
||||||
|
logger.log('');
|
||||||
|
logger.logTable(upsColumns, upsRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show groups
|
// Groups Table
|
||||||
if (config.groups && config.groups.length > 0) {
|
if (config.groups && config.groups.length > 0) {
|
||||||
logger.logBoxTitle('UPS Groups', boxWidth);
|
const groupRows = config.groups.map((group) => {
|
||||||
for (const group of config.groups) {
|
|
||||||
logger.logBoxLine(`${group.name} (${group.id}):`);
|
|
||||||
logger.logBoxLine(` Mode: ${group.mode}`);
|
|
||||||
if (group.description) {
|
|
||||||
logger.logBoxLine(` Description: ${group.description}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// List UPS devices in this group
|
|
||||||
const upsInGroup = config.upsDevices.filter((ups) =>
|
const upsInGroup = config.upsDevices.filter((ups) =>
|
||||||
ups.groups && ups.groups.includes(group.id)
|
ups.groups && ups.groups.includes(group.id)
|
||||||
);
|
);
|
||||||
logger.logBoxLine(
|
return {
|
||||||
` UPS Devices: ${
|
name: group.name,
|
||||||
upsInGroup.length > 0 ? upsInGroup.map((ups) => ups.name).join(', ') : 'None'
|
id: theme.dim(group.id),
|
||||||
}`,
|
mode: group.mode,
|
||||||
);
|
upsCount: String(upsInGroup.length),
|
||||||
logger.logBoxLine('');
|
ups: upsInGroup.length > 0
|
||||||
}
|
? upsInGroup.map((ups) => ups.name).join(', ')
|
||||||
logger.logBoxEnd();
|
: theme.dim('None'),
|
||||||
|
description: group.description || theme.dim('—'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupColumns: ITableColumn[] = [
|
||||||
|
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
|
{ header: 'ID', key: 'id', align: 'left' },
|
||||||
|
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
|
||||||
|
{ header: 'UPS', key: 'upsCount', align: 'right' },
|
||||||
|
{ header: 'UPS Devices', key: 'ups', align: 'left' },
|
||||||
|
{ header: 'Description', key: 'description', align: 'left' },
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.info(`UPS Groups (${config.groups.length}):`);
|
||||||
|
logger.log('');
|
||||||
|
logger.logTable(groupColumns, groupRows);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy single UPS configuration
|
// === Legacy Single UPS Configuration ===
|
||||||
if (!config.snmp) {
|
|
||||||
logger.logBoxLine('Error: Legacy configuration missing SNMP settings');
|
if (!config.snmp || !config.thresholds) {
|
||||||
} else {
|
logger.logBox('Configuration Error', [
|
||||||
// SNMP Settings
|
'Error: Legacy configuration missing SNMP or threshold settings',
|
||||||
logger.logBoxLine('SNMP Settings:');
|
], 60, 'error');
|
||||||
logger.logBoxLine(` Host: ${config.snmp.host}`);
|
return;
|
||||||
logger.logBoxLine(` Port: ${config.snmp.port}`);
|
|
||||||
logger.logBoxLine(` Version: ${config.snmp.version}`);
|
|
||||||
logger.logBoxLine(` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
|
||||||
|
|
||||||
if (config.snmp.version === 1 || config.snmp.version === 2) {
|
|
||||||
logger.logBoxLine(` Community: ${config.snmp.community}`);
|
|
||||||
} else if (config.snmp.version === 3) {
|
|
||||||
logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`);
|
|
||||||
logger.logBoxLine(` Username: ${config.snmp.username}`);
|
|
||||||
|
|
||||||
// Show auth and privacy details based on security level
|
|
||||||
if (
|
|
||||||
config.snmp.securityLevel === 'authNoPriv' ||
|
|
||||||
config.snmp.securityLevel === 'authPriv'
|
|
||||||
) {
|
|
||||||
logger.logBoxLine(` Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.snmp.securityLevel === 'authPriv') {
|
|
||||||
logger.logBoxLine(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show timeout value
|
|
||||||
logger.logBoxLine(` Timeout: ${config.snmp.timeout / 1000} seconds`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show OIDs if custom model is selected
|
|
||||||
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
|
|
||||||
logger.logBoxLine('Custom OIDs:');
|
|
||||||
logger.logBoxLine(
|
|
||||||
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
|
|
||||||
);
|
|
||||||
logger.logBoxLine(
|
|
||||||
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
|
||||||
);
|
|
||||||
logger.logBoxLine(
|
|
||||||
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thresholds
|
logger.log('');
|
||||||
if (!config.thresholds) {
|
logger.logBox('NUPST Configuration (Legacy)', [
|
||||||
logger.logBoxLine('Error: Legacy configuration missing threshold settings');
|
theme.warning('Legacy single-UPS configuration format'),
|
||||||
} else {
|
'',
|
||||||
logger.logBoxLine('Thresholds:');
|
theme.dim('SNMP Settings:'),
|
||||||
logger.logBoxLine(` Battery: ${config.thresholds.battery}%`);
|
` Host: ${theme.info(config.snmp.host)}`,
|
||||||
logger.logBoxLine(` Runtime: ${config.thresholds.runtime} minutes`);
|
` Port: ${theme.info(String(config.snmp.port))}`,
|
||||||
}
|
` Version: ${config.snmp.version}`,
|
||||||
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
|
` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
|
||||||
|
...(config.snmp.version === 1 || config.snmp.version === 2
|
||||||
// Configuration file location
|
? [` Community: ${config.snmp.community}`]
|
||||||
logger.logBoxLine('');
|
: []
|
||||||
logger.logBoxLine('Configuration File Location:');
|
),
|
||||||
logger.logBoxLine(' /etc/nupst/config.json');
|
...(config.snmp.version === 3
|
||||||
logger.logBoxLine('');
|
? [
|
||||||
logger.logBoxLine('Note: Using legacy single-UPS configuration format.');
|
` Security Level: ${config.snmp.securityLevel}`,
|
||||||
logger.logBoxLine('Consider using "nupst add" to migrate to multi-UPS format.');
|
` Username: ${config.snmp.username}`,
|
||||||
|
...(config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv'
|
||||||
logger.logBoxEnd();
|
? [` Auth Protocol: ${config.snmp.authProtocol || 'None'}`]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
...(config.snmp.securityLevel === 'authPriv'
|
||||||
|
? [` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
` Timeout: ${config.snmp.timeout / 1000} seconds`,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs
|
||||||
|
? [
|
||||||
|
theme.dim('Custom OIDs:'),
|
||||||
|
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
|
||||||
|
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||||
|
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
'',
|
||||||
|
theme.dim('Thresholds:'),
|
||||||
|
` Battery: ${theme.highlight(String(config.thresholds.battery))}%`,
|
||||||
|
` Runtime: ${theme.highlight(String(config.thresholds.runtime))} minutes`,
|
||||||
|
` Check Interval: ${config.checkInterval / 1000} seconds`,
|
||||||
|
'',
|
||||||
|
theme.dim('Configuration File:'),
|
||||||
|
` ${theme.path('/etc/nupst/config.json')}`,
|
||||||
|
'',
|
||||||
|
theme.warning('Note: Using legacy single-UPS configuration format.'),
|
||||||
|
`Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`,
|
||||||
|
], 70, 'warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show service status
|
// Service Status
|
||||||
try {
|
try {
|
||||||
const isActive =
|
const isActive =
|
||||||
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
||||||
const isEnabled =
|
const isEnabled =
|
||||||
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
|
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
|
||||||
|
|
||||||
const statusBoxWidth = 45;
|
logger.log('');
|
||||||
logger.logBoxTitle('Service Status', statusBoxWidth);
|
logger.logBox('Service Status', [
|
||||||
logger.logBoxLine(`Service Active: ${isActive ? 'Yes' : 'No'}`);
|
`Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
|
||||||
logger.logBoxLine(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`);
|
`Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
|
||||||
logger.logBoxEnd();
|
], 50, isActive ? 'success' : 'default');
|
||||||
|
logger.log('');
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// Ignore errors checking service status
|
// Ignore errors checking service status
|
||||||
}
|
}
|
||||||
@@ -469,7 +479,7 @@ export class NupstCli {
|
|||||||
private showVersion(): void {
|
private showVersion(): void {
|
||||||
const version = this.nupst.getVersion();
|
const version = this.nupst.getVersion();
|
||||||
logger.log(`NUPST version ${version}`);
|
logger.log(`NUPST version ${version}`);
|
||||||
logger.log('Deno-powered UPS monitoring tool');
|
logger.log('Network UPS Shutdown Tool (https://nupst.serve.zone)');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { Nupst } from '../nupst.ts';
|
import { Nupst } from '../nupst.ts';
|
||||||
import { logger } from '../logger.ts';
|
import { logger, type ITableColumn } from '../logger.ts';
|
||||||
|
import { theme } from '../colors.ts';
|
||||||
import * as helpers from '../helpers/index.ts';
|
import * as helpers from '../helpers/index.ts';
|
||||||
import { type IGroupConfig } from '../daemon.ts';
|
import { type IGroupConfig } from '../daemon.ts';
|
||||||
|
|
||||||
@@ -28,11 +29,10 @@ export class GroupHandler {
|
|||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorBoxWidth = 45;
|
logger.logBox('Configuration Error', [
|
||||||
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
'No configuration found.',
|
||||||
logger.logBoxLine('No configuration found.');
|
"Please run 'nupst ups add' first to create a configuration.",
|
||||||
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
], 50, 'error');
|
||||||
logger.logBoxEnd();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,43 +41,53 @@ export class GroupHandler {
|
|||||||
|
|
||||||
// Check if multi-UPS config
|
// Check if multi-UPS config
|
||||||
if (!config.groups || !Array.isArray(config.groups)) {
|
if (!config.groups || !Array.isArray(config.groups)) {
|
||||||
// Legacy or missing groups configuration
|
logger.logBox('UPS Groups', [
|
||||||
const boxWidth = 45;
|
'No groups configured.',
|
||||||
logger.logBoxTitle('UPS Groups', boxWidth);
|
'',
|
||||||
logger.logBoxLine('No groups configured.');
|
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
|
||||||
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
], 50, 'info');
|
||||||
logger.logBoxEnd();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display group list
|
// Display group list with modern table
|
||||||
const boxWidth = 60;
|
|
||||||
logger.logBoxTitle('UPS Groups', boxWidth);
|
|
||||||
|
|
||||||
if (config.groups.length === 0) {
|
if (config.groups.length === 0) {
|
||||||
logger.logBoxLine('No UPS groups configured.');
|
logger.logBox('UPS Groups', [
|
||||||
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
'No UPS groups configured.',
|
||||||
} else {
|
'',
|
||||||
logger.logBoxLine(`Found ${config.groups.length} group(s)`);
|
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
|
||||||
logger.logBoxLine('');
|
], 60, 'info');
|
||||||
logger.logBoxLine('ID | Name | Mode | UPS Devices');
|
return;
|
||||||
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();
|
// Prepare table data
|
||||||
|
const rows = config.groups.map((group) => {
|
||||||
|
// 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(', ');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: group.id,
|
||||||
|
name: group.name || '',
|
||||||
|
mode: group.mode || 'unknown',
|
||||||
|
count: String(upsCount),
|
||||||
|
devices: upsCount > 0 ? upsNames : theme.dim('None'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ITableColumn[] = [
|
||||||
|
{ header: 'ID', key: 'id', align: 'left', color: theme.highlight },
|
||||||
|
{ header: 'Name', key: 'name', align: 'left' },
|
||||||
|
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
|
||||||
|
{ header: 'UPS Count', key: 'count', align: 'right' },
|
||||||
|
{ header: 'UPS Devices', key: 'devices', align: 'left' },
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.info(`UPS Groups (${config.groups.length}):`);
|
||||||
|
logger.log('');
|
||||||
|
logger.logTable(columns, rows);
|
||||||
|
logger.log('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
@@ -192,6 +202,7 @@ export class GroupHandler {
|
|||||||
logger.log('\nGroup setup complete!');
|
logger.log('\nGroup setup complete!');
|
||||||
} finally {
|
} finally {
|
||||||
rl.close();
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
|
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
@@ -309,6 +320,7 @@ export class GroupHandler {
|
|||||||
logger.log('\nGroup edit complete!');
|
logger.log('\nGroup edit complete!');
|
||||||
} finally {
|
} finally {
|
||||||
rl.close();
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
|
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
@@ -366,6 +378,7 @@ export class GroupHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rl.close();
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
|
|
||||||
if (confirm !== 'y' && confirm !== 'yes') {
|
if (confirm !== 'y' && confirm !== 'yes') {
|
||||||
logger.log('Deletion cancelled.');
|
logger.log('Deletion cancelled.');
|
||||||
|
@@ -129,81 +129,57 @@ export class ServiceHandler {
|
|||||||
try {
|
try {
|
||||||
// Check if running as root
|
// Check if running as root
|
||||||
this.checkRootAccess(
|
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;
|
console.log('');
|
||||||
logger.logBoxTitle('NUPST Update Process', boxWidth);
|
logger.info('Checking for updates...');
|
||||||
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 {
|
try {
|
||||||
// 1. Update the repository
|
// Get current version
|
||||||
logger.logBoxLine('Pulling latest changes from git repository...');
|
const currentVersion = this.nupst.getVersion();
|
||||||
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, {
|
|
||||||
stdio: 'pipe',
|
// 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
|
console.log('');
|
||||||
logger.logBoxLine('Running install.sh to update NUPST...');
|
logger.success(`Updated to ${latestVersion}`);
|
||||||
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
|
console.log('');
|
||||||
|
|
||||||
// 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) {
|
} catch (error) {
|
||||||
logger.logBoxLine('Error during update process:');
|
console.log('');
|
||||||
logger.logBoxLine(`${error instanceof Error ? error.message : String(error)}`);
|
logger.error('Update failed');
|
||||||
logger.logBoxEnd();
|
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
console.log('');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -237,9 +213,11 @@ export class ServiceHandler {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('\nNUPST Uninstaller');
|
logger.log('');
|
||||||
console.log('===============');
|
logger.highlight('NUPST Uninstaller');
|
||||||
console.log('This will completely remove NUPST from your system.\n');
|
logger.dim('===============');
|
||||||
|
logger.log('This will completely remove NUPST from your system.');
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
// Ask about removing configuration
|
// Ask about removing configuration
|
||||||
const removeConfig = await prompt(
|
const removeConfig = await prompt(
|
||||||
@@ -275,17 +253,20 @@ export class ServiceHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!uninstallScriptPath) {
|
if (!uninstallScriptPath) {
|
||||||
console.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
||||||
rl.close();
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close readline before executing script
|
// Close readline before executing script
|
||||||
rl.close();
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
|
|
||||||
// Execute uninstall.sh with the appropriate option
|
// 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
|
// Pass the configuration removal option as an environment variable
|
||||||
const env = {
|
const env = {
|
||||||
@@ -301,7 +282,7 @@ export class ServiceHandler {
|
|||||||
stdio: 'inherit', // Show output in the terminal
|
stdio: 'inherit', // Show output in the terminal
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { Nupst } from '../nupst.ts';
|
import { Nupst } from '../nupst.ts';
|
||||||
import { logger } from '../logger.ts';
|
import { logger, type ITableColumn } from '../logger.ts';
|
||||||
|
import { theme } from '../colors.ts';
|
||||||
import * as helpers from '../helpers/index.ts';
|
import * as helpers from '../helpers/index.ts';
|
||||||
import type { TUpsModel } from '../snmp/types.ts';
|
import type { TUpsModel } from '../snmp/types.ts';
|
||||||
import type { INupstConfig } from '../daemon.ts';
|
import type { INupstConfig } from '../daemon.ts';
|
||||||
@@ -47,6 +48,7 @@ export class UpsHandler {
|
|||||||
await this.runAddProcess(prompt);
|
await this.runAddProcess(prompt);
|
||||||
} finally {
|
} finally {
|
||||||
rl.close();
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
@@ -178,6 +180,7 @@ export class UpsHandler {
|
|||||||
await this.runEditProcess(upsId, prompt);
|
await this.runEditProcess(upsId, prompt);
|
||||||
} finally {
|
} finally {
|
||||||
rl.close();
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
@@ -344,6 +347,7 @@ export class UpsHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rl.close();
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
|
|
||||||
if (confirm !== 'y' && confirm !== 'yes') {
|
if (confirm !== 'y' && confirm !== 'yes') {
|
||||||
logger.log('Deletion cancelled.');
|
logger.log('Deletion cancelled.');
|
||||||
@@ -376,11 +380,10 @@ export class UpsHandler {
|
|||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorBoxWidth = 45;
|
logger.logBox('Configuration Error', [
|
||||||
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
'No configuration found.',
|
||||||
logger.logBoxLine('No configuration found.');
|
"Please run 'nupst ups add' first to create a configuration.",
|
||||||
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
], 50, 'error');
|
||||||
logger.logBoxEnd();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,58 +393,57 @@ export class UpsHandler {
|
|||||||
// Check if multi-UPS config
|
// Check if multi-UPS config
|
||||||
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
||||||
// Legacy single UPS configuration
|
// Legacy single UPS configuration
|
||||||
const boxWidth = 45;
|
logger.logBox('UPS Devices', [
|
||||||
logger.logBoxTitle('UPS Devices', boxWidth);
|
'Legacy single-UPS configuration detected.',
|
||||||
logger.logBoxLine('Legacy single-UPS configuration detected.');
|
'',
|
||||||
if (!config.snmp || !config.thresholds) {
|
...((!config.snmp || !config.thresholds)
|
||||||
logger.logBoxLine('');
|
? ['Error: Configuration missing SNMP or threshold settings']
|
||||||
logger.logBoxLine('Error: Configuration missing SNMP or threshold settings');
|
: [
|
||||||
logger.logBoxEnd();
|
'Default UPS:',
|
||||||
return;
|
` Host: ${config.snmp.host}:${config.snmp.port}`,
|
||||||
}
|
` Model: ${config.snmp.upsModel || 'cyberpower'}`,
|
||||||
logger.logBoxLine('');
|
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
|
||||||
logger.logBoxLine('Default UPS:');
|
'',
|
||||||
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`);
|
'Use "nupst ups add" to add more UPS devices and migrate',
|
||||||
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
'to the multi-UPS configuration format.',
|
||||||
logger.logBoxLine(
|
]
|
||||||
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
|
),
|
||||||
);
|
], 60, 'warning');
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display UPS list
|
// Display UPS list with modern table
|
||||||
const boxWidth = 60;
|
|
||||||
logger.logBoxTitle('UPS Devices', boxWidth);
|
|
||||||
|
|
||||||
if (config.upsDevices.length === 0) {
|
if (config.upsDevices.length === 0) {
|
||||||
logger.logBoxLine('No UPS devices configured.');
|
logger.logBox('UPS Devices', [
|
||||||
logger.logBoxLine('Use "nupst add" to add a UPS device.');
|
'No UPS devices configured.',
|
||||||
} else {
|
'',
|
||||||
logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`);
|
`${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
|
||||||
logger.logBoxLine('');
|
], 60, 'info');
|
||||||
logger.logBoxLine(
|
return;
|
||||||
'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();
|
// Prepare table data
|
||||||
|
const rows = config.upsDevices.map((ups) => ({
|
||||||
|
id: ups.id,
|
||||||
|
name: ups.name || '',
|
||||||
|
host: `${ups.snmp.host}:${ups.snmp.port}`,
|
||||||
|
model: ups.snmp.upsModel || 'cyberpower',
|
||||||
|
groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const columns: ITableColumn[] = [
|
||||||
|
{ header: 'ID', key: 'id', align: 'left', color: theme.highlight },
|
||||||
|
{ header: 'Name', key: 'name', align: 'left' },
|
||||||
|
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
||||||
|
{ header: 'Model', key: 'model', align: 'left' },
|
||||||
|
{ header: 'Groups', key: 'groups', align: 'left' },
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.info(`UPS Devices (${config.upsDevices.length}):`);
|
||||||
|
logger.log('');
|
||||||
|
logger.logTable(columns, rows);
|
||||||
|
logger.log('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
@@ -667,10 +669,11 @@ export class UpsHandler {
|
|||||||
|
|
||||||
// SNMP Version
|
// SNMP Version
|
||||||
const defaultVersion = snmpConfig.version || 1;
|
const defaultVersion = snmpConfig.version || 1;
|
||||||
console.log('\nSNMP Version:');
|
logger.log('');
|
||||||
console.log(' 1) SNMPv1');
|
logger.info('SNMP Version:');
|
||||||
console.log(' 2) SNMPv2c');
|
logger.dim(' 1) SNMPv1');
|
||||||
console.log(' 3) SNMPv3 (with security features)');
|
logger.dim(' 2) SNMPv2c');
|
||||||
|
logger.dim(' 3) SNMPv3 (with security features)');
|
||||||
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
|
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
|
||||||
const version = parseInt(versionInput, 10);
|
const version = parseInt(versionInput, 10);
|
||||||
snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
|
snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
|
||||||
@@ -697,13 +700,15 @@ export class UpsHandler {
|
|||||||
snmpConfig: any,
|
snmpConfig: any,
|
||||||
prompt: (question: string) => Promise<string>,
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('\nSNMPv3 Security Settings:');
|
logger.log('');
|
||||||
|
logger.info('SNMPv3 Security Settings:');
|
||||||
|
|
||||||
// Security Level
|
// Security Level
|
||||||
console.log('\nSecurity Level:');
|
logger.log('');
|
||||||
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)');
|
logger.info('Security Level:');
|
||||||
console.log(' 2) authNoPriv (Authentication, No Privacy)');
|
logger.dim(' 1) noAuthNoPriv (No Authentication, No Privacy)');
|
||||||
console.log(' 3) authPriv (Authentication and Privacy)');
|
logger.dim(' 2) authNoPriv (Authentication, No Privacy)');
|
||||||
|
logger.dim(' 3) authPriv (Authentication and Privacy)');
|
||||||
const defaultSecLevel = snmpConfig.securityLevel
|
const defaultSecLevel = snmpConfig.securityLevel
|
||||||
? snmpConfig.securityLevel === 'noAuthNoPriv'
|
? snmpConfig.securityLevel === 'noAuthNoPriv'
|
||||||
? 1
|
? 1
|
||||||
@@ -752,8 +757,9 @@ export class UpsHandler {
|
|||||||
|
|
||||||
// Allow customizing the timeout value
|
// Allow customizing the timeout value
|
||||||
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
|
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
|
||||||
console.log(
|
logger.log('');
|
||||||
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.',
|
logger.info(
|
||||||
|
'SNMPv3 operations with authentication and privacy may require longer timeouts.',
|
||||||
);
|
);
|
||||||
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
|
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
|
||||||
const timeout = parseInt(timeoutInput, 10);
|
const timeout = parseInt(timeoutInput, 10);
|
||||||
@@ -773,9 +779,10 @@ export class UpsHandler {
|
|||||||
prompt: (question: string) => Promise<string>,
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Authentication protocol
|
// Authentication protocol
|
||||||
console.log('\nAuthentication Protocol:');
|
logger.log('');
|
||||||
console.log(' 1) MD5');
|
logger.info('Authentication Protocol:');
|
||||||
console.log(' 2) SHA');
|
logger.dim(' 1) MD5');
|
||||||
|
logger.dim(' 2) SHA');
|
||||||
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
|
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
|
||||||
const authProtocolInput = await prompt(
|
const authProtocolInput = await prompt(
|
||||||
`Select Authentication Protocol [${defaultAuthProtocol}]: `,
|
`Select Authentication Protocol [${defaultAuthProtocol}]: `,
|
||||||
@@ -799,9 +806,10 @@ export class UpsHandler {
|
|||||||
prompt: (question: string) => Promise<string>,
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Privacy protocol
|
// Privacy protocol
|
||||||
console.log('\nPrivacy Protocol:');
|
logger.log('');
|
||||||
console.log(' 1) DES');
|
logger.info('Privacy Protocol:');
|
||||||
console.log(' 2) AES');
|
logger.dim(' 1) DES');
|
||||||
|
logger.dim(' 2) AES');
|
||||||
const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1;
|
const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1;
|
||||||
const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
|
const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
|
||||||
const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
|
const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
|
||||||
@@ -822,7 +830,8 @@ export class UpsHandler {
|
|||||||
thresholds: any,
|
thresholds: any,
|
||||||
prompt: (question: string) => Promise<string>,
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('\nShutdown Thresholds:');
|
logger.log('');
|
||||||
|
logger.info('Shutdown Thresholds:');
|
||||||
|
|
||||||
// Battery threshold
|
// Battery threshold
|
||||||
const defaultBatteryThreshold = thresholds.battery || 60;
|
const defaultBatteryThreshold = thresholds.battery || 60;
|
||||||
@@ -854,13 +863,14 @@ export class UpsHandler {
|
|||||||
snmpConfig: any,
|
snmpConfig: any,
|
||||||
prompt: (question: string) => Promise<string>,
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('\nUPS Model Selection:');
|
logger.log('');
|
||||||
console.log(' 1) CyberPower');
|
logger.info('UPS Model Selection:');
|
||||||
console.log(' 2) APC');
|
logger.dim(' 1) CyberPower');
|
||||||
console.log(' 3) Eaton');
|
logger.dim(' 2) APC');
|
||||||
console.log(' 4) TrippLite');
|
logger.dim(' 3) Eaton');
|
||||||
console.log(' 5) Liebert/Vertiv');
|
logger.dim(' 4) TrippLite');
|
||||||
console.log(' 6) Custom (Advanced)');
|
logger.dim(' 5) Liebert/Vertiv');
|
||||||
|
logger.dim(' 6) Custom (Advanced)');
|
||||||
|
|
||||||
const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
|
const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
|
||||||
? 1
|
? 1
|
||||||
@@ -891,8 +901,9 @@ export class UpsHandler {
|
|||||||
snmpConfig.upsModel = 'liebert';
|
snmpConfig.upsModel = 'liebert';
|
||||||
} else if (modelValue === 6) {
|
} else if (modelValue === 6) {
|
||||||
snmpConfig.upsModel = 'custom';
|
snmpConfig.upsModel = 'custom';
|
||||||
console.log('\nEnter custom OIDs for your UPS:');
|
logger.log('');
|
||||||
console.log('(Leave blank to use standard RFC 1628 OIDs as fallback)');
|
logger.info('Enter custom OIDs for your UPS:');
|
||||||
|
logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)');
|
||||||
|
|
||||||
// Custom OIDs
|
// Custom OIDs
|
||||||
const powerStatusOID = await prompt('Power Status OID: ');
|
const powerStatusOID = await prompt('Power Status OID: ');
|
||||||
|
213
ts/daemon.ts
213
ts/daemon.ts
@@ -5,8 +5,9 @@ import { exec, execFile } from 'node:child_process';
|
|||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import { NupstSnmp } from './snmp/manager.ts';
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import type { ISnmpConfig } from './snmp/types.ts';
|
import type { ISnmpConfig } from './snmp/types.ts';
|
||||||
import { logger } from './logger.ts';
|
import { logger, type ITableColumn } from './logger.ts';
|
||||||
import { MigrationRunner } from './migrations/index.ts';
|
import { MigrationRunner } from './migrations/index.ts';
|
||||||
|
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
@@ -207,11 +208,9 @@ export class NupstDaemon {
|
|||||||
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2));
|
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2));
|
||||||
this.config = configToSave;
|
this.config = configToSave;
|
||||||
|
|
||||||
console.log('┌─ Configuration Saved ─────────────────────┐');
|
logger.logBox('Configuration Saved', [`Location: ${this.CONFIG_PATH}`], 45, 'success');
|
||||||
console.log(`│ Location: ${this.CONFIG_PATH}`);
|
|
||||||
console.log('└──────────────────────────────────────────┘');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving configuration:', error);
|
logger.error(`Error saving configuration: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +218,7 @@ export class NupstDaemon {
|
|||||||
* Helper method to log configuration errors consistently
|
* Helper method to log configuration errors consistently
|
||||||
*/
|
*/
|
||||||
private logConfigError(message: string): void {
|
private logConfigError(message: string): void {
|
||||||
console.error('┌─ Configuration Error ─────────────────────┐');
|
logger.logBox('Configuration Error', [message, "Please run 'nupst setup' first to create a configuration."], 45, 'error');
|
||||||
console.error(`│ ${message}`);
|
|
||||||
console.error("│ Please run 'nupst setup' first to create a configuration.");
|
|
||||||
console.error('└───────────────────────────────────────────┘');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -315,29 +311,57 @@ export class NupstDaemon {
|
|||||||
* Log the loaded configuration settings
|
* Log the loaded configuration settings
|
||||||
*/
|
*/
|
||||||
private logConfigLoaded(): void {
|
private logConfigLoaded(): void {
|
||||||
const boxWidth = 50;
|
|
||||||
logger.logBoxTitle('Configuration Loaded', boxWidth);
|
logger.log('');
|
||||||
|
logger.logBoxTitle('Configuration Loaded', 70, 'success');
|
||||||
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
|
||||||
logger.logBoxLine(`UPS Devices: ${this.config.upsDevices.length}`);
|
|
||||||
for (const ups of this.config.upsDevices) {
|
|
||||||
logger.logBoxLine(` - ${ups.name} (${ups.id}): ${ups.snmp.host}:${ups.snmp.port}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.logBoxLine('No UPS devices configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.config.groups && this.config.groups.length > 0) {
|
|
||||||
logger.logBoxLine(`Groups: ${this.config.groups.length}`);
|
|
||||||
for (const group of this.config.groups) {
|
|
||||||
logger.logBoxLine(` - ${group.name} (${group.id}): ${group.mode} mode`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.logBoxLine('No Groups configured');
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
// Display UPS devices in a table
|
||||||
|
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
||||||
|
logger.info(`UPS Devices (${this.config.upsDevices.length}):`);
|
||||||
|
|
||||||
|
const upsColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
||||||
|
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
|
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
||||||
|
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
||||||
|
{ header: 'Battery/Runtime', key: 'thresholds', align: 'left' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const upsRows: Array<Record<string, string>> = this.config.upsDevices.map((ups) => ({
|
||||||
|
name: ups.name,
|
||||||
|
id: ups.id,
|
||||||
|
host: `${ups.snmp.host}:${ups.snmp.port}`,
|
||||||
|
thresholds: `${ups.thresholds.battery}% / ${ups.thresholds.runtime} min`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.logTable(upsColumns, upsRows);
|
||||||
|
logger.log('');
|
||||||
|
} else {
|
||||||
|
logger.warn('No UPS devices configured');
|
||||||
|
logger.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display groups in a table
|
||||||
|
if (this.config.groups && this.config.groups.length > 0) {
|
||||||
|
logger.info(`Groups (${this.config.groups.length}):`);
|
||||||
|
|
||||||
|
const groupColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
||||||
|
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
|
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
||||||
|
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
|
||||||
|
];
|
||||||
|
|
||||||
|
const groupRows: Array<Record<string, string>> = this.config.groups.map((group) => ({
|
||||||
|
name: group.name,
|
||||||
|
id: group.id,
|
||||||
|
mode: group.mode,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.logTable(groupColumns, groupRows);
|
||||||
|
logger.log('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -433,9 +457,13 @@ export class NupstDaemon {
|
|||||||
|
|
||||||
// Check if power status changed
|
// Check if power status changed
|
||||||
if (currentStatus && currentStatus.powerStatus !== status.powerStatus) {
|
if (currentStatus && currentStatus.powerStatus !== status.powerStatus) {
|
||||||
logger.logBoxTitle(`Power Status Change: ${ups.name}`, 50);
|
logger.log('');
|
||||||
logger.logBoxLine(`Status changed: ${currentStatus.powerStatus} → ${status.powerStatus}`);
|
logger.logBoxTitle(`Power Status Change: ${ups.name}`, 60, 'warning');
|
||||||
|
logger.logBoxLine(`Previous: ${formatPowerStatus(currentStatus.powerStatus)}`);
|
||||||
|
logger.logBoxLine(`Current: ${formatPowerStatus(status.powerStatus)}`);
|
||||||
|
logger.logBoxLine(`Time: ${new Date().toISOString()}`);
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
updatedStatus.lastStatusChange = currentTime;
|
updatedStatus.lastStatusChange = currentTime;
|
||||||
}
|
}
|
||||||
@@ -457,21 +485,38 @@ export class NupstDaemon {
|
|||||||
*/
|
*/
|
||||||
private logAllUpsStatus(): void {
|
private logAllUpsStatus(): void {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
const boxWidth = 60;
|
|
||||||
logger.logBoxTitle('Periodic Status Update', boxWidth);
|
logger.log('');
|
||||||
|
logger.logBoxTitle('Periodic Status Update', 70, 'info');
|
||||||
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
||||||
logger.logBoxLine('');
|
logger.logBoxEnd();
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
// Build table data
|
||||||
|
const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
||||||
|
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
|
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
||||||
|
{ header: 'Power Status', key: 'powerStatus', align: 'left' },
|
||||||
|
{ header: 'Battery', key: 'battery', align: 'right' },
|
||||||
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows: Array<Record<string, string>> = [];
|
||||||
for (const [id, status] of this.upsStatus.entries()) {
|
for (const [id, status] of this.upsStatus.entries()) {
|
||||||
logger.logBoxLine(`UPS: ${status.name} (${id})`);
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||||
logger.logBoxLine(` Power Status: ${status.powerStatus}`);
|
const runtimeColor = getRuntimeColor(status.batteryRuntime);
|
||||||
logger.logBoxLine(
|
|
||||||
` Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`,
|
rows.push({
|
||||||
);
|
name: status.name,
|
||||||
logger.logBoxLine('');
|
id: id,
|
||||||
|
powerStatus: formatPowerStatus(status.powerStatus),
|
||||||
|
battery: batteryColor(status.batteryCapacity + '%'),
|
||||||
|
runtime: runtimeColor(status.batteryRuntime + ' min'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.logBoxEnd();
|
logger.logTable(columns, rows);
|
||||||
|
logger.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -750,38 +795,61 @@ export class NupstDaemon {
|
|||||||
const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring
|
const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
logger.log(
|
logger.log('');
|
||||||
`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`,
|
logger.logBoxTitle('Shutdown Monitoring Active', 60, 'warning');
|
||||||
);
|
logger.logBoxLine(`Emergency threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes runtime`);
|
||||||
|
logger.logBoxLine(`Check interval: ${CHECK_INTERVAL / 1000} seconds`);
|
||||||
|
logger.logBoxLine(`Max monitoring time: ${MAX_MONITORING_TIME / 1000} seconds`);
|
||||||
|
logger.logBoxEnd();
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
// Continue monitoring until max monitoring time is reached
|
// Continue monitoring until max monitoring time is reached
|
||||||
while (Date.now() - startTime < MAX_MONITORING_TIME) {
|
while (Date.now() - startTime < MAX_MONITORING_TIME) {
|
||||||
try {
|
try {
|
||||||
logger.log('Checking UPS status during shutdown...');
|
logger.info('Checking UPS status during shutdown...');
|
||||||
|
|
||||||
|
// Build table for UPS status during shutdown
|
||||||
|
const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
||||||
|
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
|
{ header: 'Battery', key: 'battery', align: 'right' },
|
||||||
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||||
|
{ header: 'Status', key: 'status', align: 'left' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows: Array<Record<string, string>> = [];
|
||||||
|
let emergencyDetected = false;
|
||||||
|
let emergencyUps: any = null;
|
||||||
|
|
||||||
// Check all UPS devices
|
// Check all UPS devices
|
||||||
for (const ups of this.config.upsDevices) {
|
for (const ups of this.config.upsDevices) {
|
||||||
try {
|
try {
|
||||||
const status = await this.snmp.getUpsStatus(ups.snmp);
|
const status = await this.snmp.getUpsStatus(ups.snmp);
|
||||||
|
|
||||||
logger.log(
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||||
`UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`,
|
const runtimeColor = getRuntimeColor(status.batteryRuntime);
|
||||||
);
|
|
||||||
|
|
||||||
// If any UPS battery runtime gets critically low, force immediate shutdown
|
const isCritical = status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD;
|
||||||
if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) {
|
|
||||||
logger.logBoxTitle('EMERGENCY SHUTDOWN', 50);
|
rows.push({
|
||||||
logger.logBoxLine(
|
name: ups.name,
|
||||||
`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`,
|
battery: batteryColor(status.batteryCapacity + '%'),
|
||||||
);
|
runtime: runtimeColor(status.batteryRuntime + ' min'),
|
||||||
logger.logBoxLine('Forcing immediate shutdown!');
|
status: isCritical ? theme.error('CRITICAL!') : theme.success('OK'),
|
||||||
logger.logBoxEnd();
|
});
|
||||||
|
|
||||||
// Force immediate shutdown
|
// If any UPS battery runtime gets critically low, flag for immediate shutdown
|
||||||
await this.forceImmediateShutdown();
|
if (isCritical && !emergencyDetected) {
|
||||||
return;
|
emergencyDetected = true;
|
||||||
|
emergencyUps = { ups, status };
|
||||||
}
|
}
|
||||||
} catch (upsError) {
|
} catch (upsError) {
|
||||||
|
rows.push({
|
||||||
|
name: ups.name,
|
||||||
|
battery: theme.error('N/A'),
|
||||||
|
runtime: theme.error('N/A'),
|
||||||
|
status: theme.error('ERROR'),
|
||||||
|
});
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error checking UPS ${ups.name} during shutdown: ${
|
`Error checking UPS ${ups.name} during shutdown: ${
|
||||||
upsError instanceof Error ? upsError.message : String(upsError)
|
upsError instanceof Error ? upsError.message : String(upsError)
|
||||||
@@ -790,6 +858,27 @@ export class NupstDaemon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display the table
|
||||||
|
logger.logTable(columns, rows);
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
// If emergency detected, trigger immediate shutdown
|
||||||
|
if (emergencyDetected && emergencyUps) {
|
||||||
|
logger.log('');
|
||||||
|
logger.logBoxTitle('EMERGENCY SHUTDOWN', 60, 'error');
|
||||||
|
logger.logBoxLine(
|
||||||
|
`UPS ${emergencyUps.ups.name} runtime critically low: ${emergencyUps.status.batteryRuntime} minutes`,
|
||||||
|
);
|
||||||
|
logger.logBoxLine(`Emergency threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes`);
|
||||||
|
logger.logBoxLine('Forcing immediate shutdown!');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
// Force immediate shutdown
|
||||||
|
await this.forceImmediateShutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait before checking again
|
// Wait before checking again
|
||||||
await this.sleep(CHECK_INTERVAL);
|
await this.sleep(CHECK_INTERVAL);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -802,7 +891,9 @@ export class NupstDaemon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('UPS monitoring during shutdown completed');
|
logger.log('');
|
||||||
|
logger.success('UPS monitoring during shutdown completed');
|
||||||
|
logger.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -34,12 +34,14 @@ export class MigrationRunner {
|
|||||||
let currentConfig = config;
|
let currentConfig = config;
|
||||||
let anyMigrationsRan = false;
|
let anyMigrationsRan = false;
|
||||||
|
|
||||||
logger.dim('Checking for required config migrations...');
|
|
||||||
|
|
||||||
for (const migration of this.migrations) {
|
for (const migration of this.migrations) {
|
||||||
const shouldRun = await migration.shouldRun(currentConfig);
|
const shouldRun = await migration.shouldRun(currentConfig);
|
||||||
|
|
||||||
if (shouldRun) {
|
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()}...`);
|
logger.info(`Running ${migration.getName()}...`);
|
||||||
currentConfig = await migration.migrate(currentConfig);
|
currentConfig = await migration.migrate(currentConfig);
|
||||||
anyMigrationsRan = true;
|
anyMigrationsRan = true;
|
||||||
@@ -49,7 +51,7 @@ export class MigrationRunner {
|
|||||||
if (anyMigrationsRan) {
|
if (anyMigrationsRan) {
|
||||||
logger.success('Configuration migrations complete');
|
logger.success('Configuration migrations complete');
|
||||||
} else {
|
} else {
|
||||||
logger.dim('No migrations needed');
|
logger.success('config format ok');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -525,6 +525,7 @@ export class NupstSnmp {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine power status based on UPS model and raw value
|
* Determine power status based on UPS model and raw value
|
||||||
|
* Uses the value mappings defined in the OID sets
|
||||||
* @param upsModel UPS model
|
* @param upsModel UPS model
|
||||||
* @param powerStatusValue Raw power status value
|
* @param powerStatusValue Raw power status value
|
||||||
* @returns Standardized power status
|
* @returns Standardized power status
|
||||||
@@ -533,39 +534,28 @@ export class NupstSnmp {
|
|||||||
upsModel: TUpsModel | undefined,
|
upsModel: TUpsModel | undefined,
|
||||||
powerStatusValue: number,
|
powerStatusValue: number,
|
||||||
): 'online' | 'onBattery' | 'unknown' {
|
): 'online' | 'onBattery' | 'unknown' {
|
||||||
if (upsModel === 'cyberpower') {
|
// Get the OID set for this UPS model
|
||||||
// CyberPower RMCARD205: upsBaseOutputStatus values
|
if (upsModel && upsModel !== 'custom') {
|
||||||
// 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc.
|
const oidSet = UpsOidSets.getOidSet(upsModel);
|
||||||
if (powerStatusValue === 2) {
|
|
||||||
return 'online';
|
// Use the value mappings if available
|
||||||
} else if (powerStatusValue === 3) {
|
if (oidSet.POWER_STATUS_VALUES) {
|
||||||
return 'onBattery';
|
if (powerStatusValue === oidSet.POWER_STATUS_VALUES.online) {
|
||||||
}
|
return 'online';
|
||||||
} else if (upsModel === 'eaton') {
|
} else if (powerStatusValue === oidSet.POWER_STATUS_VALUES.onBattery) {
|
||||||
// Eaton UPS: xupsOutputSource values
|
return 'onBattery';
|
||||||
// 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';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,37 +11,57 @@ export class UpsOidSets {
|
|||||||
private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = {
|
private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = {
|
||||||
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
|
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
|
||||||
cyberpower: {
|
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_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)
|
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 OIDs
|
||||||
apc: {
|
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_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
|
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 OIDs
|
||||||
eaton: {
|
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_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)
|
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 OIDs
|
||||||
tripplite: {
|
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_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
|
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/Vertiv OIDs
|
||||||
liebert: {
|
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_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
|
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)
|
// Custom OIDs (to be provided by the user)
|
||||||
|
@@ -28,6 +28,13 @@ export interface IOidSet {
|
|||||||
BATTERY_CAPACITY: string;
|
BATTERY_CAPACITY: string;
|
||||||
/** OID for battery runtime */
|
/** OID for battery runtime */
|
||||||
BATTERY_RUNTIME: string;
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
116
ts/systemd.ts
116
ts/systemd.ts
@@ -50,11 +50,11 @@ WantedBy=multi-user.target
|
|||||||
try {
|
try {
|
||||||
await fs.access(configPath);
|
await fs.access(configPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const boxWidth = 50;
|
logger.log('');
|
||||||
logger.logBoxTitle('Configuration Error', boxWidth);
|
logger.error('No configuration found');
|
||||||
logger.logBoxLine(`No configuration file found at ${configPath}`);
|
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
|
||||||
logger.logBoxLine("Please run 'nupst add' first to create a UPS configuration.");
|
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
|
||||||
logger.logBoxEnd();
|
logger.log('');
|
||||||
throw new Error('Configuration not found');
|
throw new Error('Configuration not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,21 +134,59 @@ WantedBy=multi-user.target
|
|||||||
* Get status of the systemd service and UPS
|
* Get status of the systemd service and UPS
|
||||||
* @param debugMode Whether to enable debug mode for SNMP
|
* @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> {
|
public async getStatus(debugMode: boolean = false): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Enable debug mode if requested
|
// Enable debug mode if requested
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
const boxWidth = 45;
|
console.log('');
|
||||||
logger.logBoxTitle('Debug Mode', boxWidth);
|
logger.info('Debug Mode: SNMP debugging enabled');
|
||||||
logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
|
console.log('');
|
||||||
logger.logBoxEnd();
|
|
||||||
this.daemon.getNupstSnmp().enableDebug();
|
this.daemon.getNupstSnmp().enableDebug();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display version information
|
// Display version and update status first
|
||||||
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
|
await this.displayVersionInfo();
|
||||||
|
|
||||||
// Check if config exists first
|
// Check if config exists
|
||||||
try {
|
try {
|
||||||
await this.checkConfigExists();
|
await this.checkConfigExists();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -196,11 +234,11 @@ WantedBy=multi-user.target
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display beautiful status
|
// Display beautiful status
|
||||||
console.log('');
|
logger.log('');
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
console.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
|
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
|
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pid || memory || cpu) {
|
if (pid || memory || cpu) {
|
||||||
@@ -208,14 +246,14 @@ WantedBy=multi-user.target
|
|||||||
if (pid) details.push(`PID: ${theme.dim(pid)}`);
|
if (pid) details.push(`PID: ${theme.dim(pid)}`);
|
||||||
if (memory) details.push(`Memory: ${theme.dim(memory)}`);
|
if (memory) details.push(`Memory: ${theme.dim(memory)}`);
|
||||||
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
|
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
|
||||||
console.log(` ${details.join(' ')}`);
|
logger.log(` ${details.join(' ')}`);
|
||||||
}
|
}
|
||||||
console.log('');
|
logger.log('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('');
|
logger.log('');
|
||||||
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
|
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
|
||||||
console.log('');
|
logger.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +270,7 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
// Check if we have the new multi-UPS config format
|
// Check if we have the new multi-UPS config format
|
||||||
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||||
console.log(theme.info(`UPS Devices (${config.upsDevices.length}):`));
|
logger.info(`UPS Devices (${config.upsDevices.length}):`);
|
||||||
|
|
||||||
// Show status for each UPS
|
// Show status for each UPS
|
||||||
for (const ups of config.upsDevices) {
|
for (const ups of config.upsDevices) {
|
||||||
@@ -240,7 +278,7 @@ WantedBy=multi-user.target
|
|||||||
}
|
}
|
||||||
} else if (config.snmp) {
|
} else if (config.snmp) {
|
||||||
// Legacy single UPS configuration
|
// Legacy single UPS configuration
|
||||||
console.log(theme.info('UPS Devices (1):'));
|
logger.info('UPS Devices (1):');
|
||||||
const legacyUps = {
|
const legacyUps = {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
name: 'Default UPS',
|
name: 'Default UPS',
|
||||||
@@ -251,16 +289,16 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
await this.displaySingleUpsStatus(legacyUps, snmp);
|
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||||
} else {
|
} else {
|
||||||
console.log('');
|
logger.log('');
|
||||||
console.log(`${symbols.warning} ${theme.warning('No UPS devices configured')}`);
|
logger.warn('No UPS devices configured');
|
||||||
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
|
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
|
||||||
console.log('');
|
logger.log('');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('');
|
logger.log('');
|
||||||
console.log(`${symbols.error} ${theme.error('Failed to retrieve UPS status')}`);
|
logger.error('Failed to retrieve UPS status');
|
||||||
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||||
console.log('');
|
logger.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,15 +326,15 @@ WantedBy=multi-user.target
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display UPS name and power status
|
// Display UPS name and power status
|
||||||
console.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
|
logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
|
||||||
|
|
||||||
// Display battery with color coding
|
// Display battery with color coding
|
||||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||||
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
|
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
|
||||||
console.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
||||||
|
|
||||||
// Display host info
|
// Display host info
|
||||||
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||||
|
|
||||||
// Display groups if any
|
// Display groups if any
|
||||||
if (ups.groups && ups.groups.length > 0) {
|
if (ups.groups && ups.groups.length > 0) {
|
||||||
@@ -305,17 +343,17 @@ WantedBy=multi-user.target
|
|||||||
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
|
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
|
||||||
return group ? group.name : groupId;
|
return group ? group.name : groupId;
|
||||||
});
|
});
|
||||||
console.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
|
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('');
|
logger.log('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Display error for this UPS
|
// Display error for this UPS
|
||||||
console.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
|
logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
|
||||||
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||||
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||||
console.log('');
|
logger.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user