Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
01ccf2d080 | |||
0e55f22dad | |||
bd3042de25 | |||
456351ca34 | |||
00afa317ef | |||
45ee8208b5 | |||
39bf3e2239 | |||
f3de3f0618 | |||
03056d279d | |||
f860f39e59 | |||
fa4516de3b | |||
539547beb8 | |||
6eb92959ec | |||
4af9af0845 | |||
f7e12cdcbb | |||
002498b91b | |||
459911fe5f | |||
9859a02ea2 | |||
65444b6d25 | |||
d049e8741f | |||
1123a99aea | |||
d01e878310 | |||
588aeabf4b |
72
changelog.md
72
changelog.md
@ -1,5 +1,77 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-03-28 - 3.0.0 - BREAKING CHANGE(core)
|
||||
Add multi-UPS support and group management; update CLI, configuration and documentation to support multiple UPS devices with group modes
|
||||
|
||||
- Implemented multi-UPS configuration with an array of UPS devices and groups in the configuration file
|
||||
- Added group management commands (group add, edit, delete, list) with redundant and non-redundant modes
|
||||
- Revamped CLI command parsing for UPS management (add, edit, delete, list, setup) and group subcommands
|
||||
- Updated readme and documentation to reflect new configuration structure and features
|
||||
- Enhanced logging and status display for multiple UPS devices
|
||||
|
||||
## 2025-03-26 - 2.6.17 - fix(logger)
|
||||
Preserve logbox width after logBoxEnd so that subsequent logBoxLine calls continue using the set width.
|
||||
|
||||
- Removed the reset of currentBoxWidth in logBoxEnd to allow persistent width across logbox calls.
|
||||
- Ensures that logBoxLine uses the previously set width when no new width is provided.
|
||||
|
||||
## 2025-03-26 - 2.6.16 - fix(cli)
|
||||
Improve CLI logging consistency by replacing direct console output with unified logger calls.
|
||||
|
||||
- Replaced console.log and console.error with logger.log and logger.error in CLI commands
|
||||
- Standardized debug, error, and status messages using logger's logbox utilities
|
||||
- Enhanced consistency of log output throughout the ts/cli.ts file
|
||||
|
||||
## 2025-03-26 - 2.6.15 - fix(logger)
|
||||
Replace direct console logging with unified logger interface for consistent formatting
|
||||
|
||||
- Substitute console.log, console.error, and related calls with logger methods in cli, daemon, systemd, nupst, and index modules
|
||||
- Integrate logBox formatting for structured output and consistent log presentation
|
||||
- Update test expectations in test.logger.ts to check for standardized error messages
|
||||
- Refactor logging calls throughout the codebase for improved clarity and maintainability
|
||||
|
||||
## 2025-03-26 - 2.6.14 - fix(systemd)
|
||||
Shorten closing log divider in systemd service installation output for consistent formatting.
|
||||
|
||||
- Replaced the overly long footer with a shorter one in ts/systemd.ts.
|
||||
- This change improves log readability without affecting functionality.
|
||||
|
||||
## 2025-03-26 - 2.6.13 - fix(cli)
|
||||
Fix CLI update output box formatting
|
||||
|
||||
- Adjusted the closing box line in the update process log messages for consistent visual formatting
|
||||
|
||||
## 2025-03-26 - 2.6.12 - fix(systemd)
|
||||
Adjust logging border in systemd service installation output
|
||||
|
||||
- Updated the closing border line for consistent output formatting in ts/systemd.ts
|
||||
|
||||
## 2025-03-26 - 2.6.11 - fix(cli, systemd)
|
||||
Adjust log formatting for consistent output in CLI and systemd commands
|
||||
|
||||
- Fixed spacing issues in service installation and status log messages in the systemd module.
|
||||
- Revised output formatting in the CLI to improve message clarity.
|
||||
|
||||
## 2025-03-26 - 2.6.10 - fix(daemon)
|
||||
Adjust console log box formatting for consistent output in daemon status messages
|
||||
|
||||
- Updated closing box borders to align properly in configuration error, periodic updates, and UPS status logs
|
||||
- Improved visual consistency in log messages
|
||||
|
||||
## 2025-03-26 - 2.6.9 - fix(cli)
|
||||
Improve console output formatting for status banners and logging messages
|
||||
|
||||
- Standardize banner messages in daemon status updates
|
||||
- Refine version information banner in nupst logging
|
||||
- Update UPS connection and status banners in systemd
|
||||
|
||||
## 2025-03-26 - 2.6.8 - fix(cli)
|
||||
Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP manager
|
||||
|
||||
- Standardize whitespace and formatting in ts/cli.ts for consistency
|
||||
- Refine argument filtering for debug mode and prompt messages
|
||||
- Remove unused 'dgram' import from ts/snmp/manager.ts
|
||||
|
||||
## 2025-03-26 - 2.6.7 - fix(setup.sh)
|
||||
Clarify net-snmp dependency installation message in setup.sh
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "2.6.7",
|
||||
"version": "3.0.0",
|
||||
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
@ -56,5 +56,6 @@
|
||||
"mongodb-memory-server",
|
||||
"puppeteer"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
||||
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -9,7 +9,7 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
net-snmp:
|
||||
specifier: ^3.20.0
|
||||
specifier: 3.20.0
|
||||
version: 3.20.0
|
||||
devDependencies:
|
||||
'@git.zone/tsbuild':
|
||||
|
162
readme.md
162
readme.md
@ -4,6 +4,10 @@ NUPST is a command-line tool that monitors SNMP-enabled UPS devices and initiate
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-UPS Support**: Monitor and manage multiple UPS devices from a single installation
|
||||
- **Group Management**: Organize UPS devices into groups with different operating modes
|
||||
- **Redundant Mode**: Only shutdown when ALL UPS devices in a group are in critical condition
|
||||
- **Non-Redundant Mode**: Shutdown when ANY UPS device in a group is in critical condition
|
||||
- Monitors UPS devices using SNMP (v1, v2c, and v3 supported)
|
||||
- Automatic shutdown when battery level falls below threshold
|
||||
- Automatic shutdown when runtime remaining falls below threshold
|
||||
@ -124,8 +128,22 @@ Usage:
|
||||
nupst stop - Stop the systemd service
|
||||
nupst start - Start the systemd service
|
||||
nupst status - Show status of the systemd service and UPS status
|
||||
nupst setup - Run the interactive setup to configure SNMP settings
|
||||
nupst test - Test the current configuration by connecting to the UPS
|
||||
|
||||
UPS Management:
|
||||
nupst add - Add a new UPS device
|
||||
nupst edit [id] - Edit an existing UPS (default UPS if no ID provided)
|
||||
nupst delete <id> - Delete a UPS by ID
|
||||
nupst list - List all configured UPS devices
|
||||
nupst setup - Alias for 'nupst edit' (backward compatibility)
|
||||
|
||||
Group Management:
|
||||
nupst group list - List all UPS groups
|
||||
nupst group add - Add a new UPS group
|
||||
nupst group edit <id> - Edit an existing UPS group
|
||||
nupst group delete <id> - Delete a UPS group
|
||||
|
||||
System Commands:
|
||||
nupst test - Test the current configuration by connecting to all UPS devices
|
||||
nupst config - Display the current configuration
|
||||
nupst update - Update NUPST from repository and refresh systemd service (requires root)
|
||||
nupst uninstall - Completely uninstall NUPST from the system (requires root)
|
||||
@ -138,62 +156,114 @@ Options:
|
||||
|
||||
## Configuration
|
||||
|
||||
NUPST provides an interactive setup to configure your UPS:
|
||||
NUPST supports monitoring multiple UPS devices organized into groups. You can set up your UPS devices using the interactive commands:
|
||||
|
||||
```bash
|
||||
nupst setup
|
||||
# Add a new UPS device
|
||||
nupst add
|
||||
|
||||
# Create a new group
|
||||
nupst group add
|
||||
|
||||
# Assign UPS devices to groups
|
||||
nupst group edit <group-id>
|
||||
```
|
||||
|
||||
This will guide you through setting up:
|
||||
- UPS IP address and SNMP settings
|
||||
- Shutdown thresholds for battery percentage and runtime
|
||||
- Monitoring interval
|
||||
- Test the connection to your UPS
|
||||
### Configuration File Structure
|
||||
|
||||
Alternatively, you can manually edit the configuration file at `/etc/nupst/config.json`. A default configuration will be created on first run:
|
||||
The configuration file is located at `/etc/nupst/config.json`. Here's an example of a multi-UPS configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"snmp": {
|
||||
"host": "192.168.1.100",
|
||||
"port": 161,
|
||||
"community": "public",
|
||||
"version": 1,
|
||||
"timeout": 5000,
|
||||
"upsModel": "cyberpower"
|
||||
},
|
||||
"thresholds": {
|
||||
"battery": 60,
|
||||
"runtime": 20
|
||||
},
|
||||
"checkInterval": 30000
|
||||
"checkInterval": 30000,
|
||||
"upsDevices": [
|
||||
{
|
||||
"id": "ups-1",
|
||||
"name": "Server Room UPS",
|
||||
"snmp": {
|
||||
"host": "192.168.1.100",
|
||||
"port": 161,
|
||||
"community": "public",
|
||||
"version": 1,
|
||||
"timeout": 5000,
|
||||
"upsModel": "cyberpower"
|
||||
},
|
||||
"thresholds": {
|
||||
"battery": 60,
|
||||
"runtime": 20
|
||||
},
|
||||
"groups": ["datacenter"]
|
||||
},
|
||||
{
|
||||
"id": "ups-2",
|
||||
"name": "Network Rack UPS",
|
||||
"snmp": {
|
||||
"host": "192.168.1.101",
|
||||
"port": 161,
|
||||
"community": "public",
|
||||
"version": 1,
|
||||
"timeout": 5000,
|
||||
"upsModel": "apc"
|
||||
},
|
||||
"thresholds": {
|
||||
"battery": 50,
|
||||
"runtime": 15
|
||||
},
|
||||
"groups": ["datacenter"]
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "datacenter",
|
||||
"name": "Data Center",
|
||||
"mode": "redundant",
|
||||
"description": "Main data center UPS group"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `snmp`: SNMP connection settings
|
||||
- `host`: IP address of your UPS (default: 127.0.0.1)
|
||||
- `port`: SNMP port (default: 161)
|
||||
- `version`: SNMP version (1, 2, or 3)
|
||||
- `timeout`: Timeout in milliseconds (default: 5000)
|
||||
- `upsModel`: The UPS model ('cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', or 'custom')
|
||||
- For SNMPv1/v2c:
|
||||
- `community`: SNMP community string (default: public)
|
||||
- For SNMPv3:
|
||||
- `securityLevel`: Security level ('noAuthNoPriv', 'authNoPriv', or 'authPriv')
|
||||
- `username`: SNMPv3 username
|
||||
- `authProtocol`: Authentication protocol ('MD5' or 'SHA')
|
||||
- `authKey`: Authentication password/key
|
||||
- `privProtocol`: Privacy/encryption protocol ('DES' or 'AES')
|
||||
- `privKey`: Privacy password/key
|
||||
- For custom UPS models:
|
||||
- `customOIDs`: Object containing custom OIDs for your UPS:
|
||||
- `POWER_STATUS`: OID for power status
|
||||
- `BATTERY_CAPACITY`: OID for battery capacity percentage
|
||||
- `BATTERY_RUNTIME`: OID for runtime remaining in minutes
|
||||
- `thresholds`: When to trigger shutdown
|
||||
- `battery`: Battery percentage threshold (default: 60%)
|
||||
- `runtime`: Runtime minutes threshold (default: 20 minutes)
|
||||
### Configuration Fields
|
||||
|
||||
- `checkInterval`: How often to check UPS status in milliseconds (default: 30000)
|
||||
- `upsDevices`: Array of UPS device configurations
|
||||
- `id`: Unique identifier for the UPS
|
||||
- `name`: Friendly name for the UPS
|
||||
- `snmp`: SNMP connection settings
|
||||
- `host`: IP address of your UPS (default: 127.0.0.1)
|
||||
- `port`: SNMP port (default: 161)
|
||||
- `version`: SNMP version (1, 2, or 3)
|
||||
- `timeout`: Timeout in milliseconds (default: 5000)
|
||||
- `upsModel`: The UPS model ('cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', or 'custom')
|
||||
- For SNMPv1/v2c:
|
||||
- `community`: SNMP community string (default: public)
|
||||
- For SNMPv3:
|
||||
- `securityLevel`: Security level ('noAuthNoPriv', 'authNoPriv', or 'authPriv')
|
||||
- `username`: SNMPv3 username
|
||||
- `authProtocol`: Authentication protocol ('MD5' or 'SHA')
|
||||
- `authKey`: Authentication password/key
|
||||
- `privProtocol`: Privacy/encryption protocol ('DES' or 'AES')
|
||||
- `privKey`: Privacy password/key
|
||||
- For custom UPS models:
|
||||
- `customOIDs`: Object containing custom OIDs for your UPS:
|
||||
- `POWER_STATUS`: OID for power status
|
||||
- `BATTERY_CAPACITY`: OID for battery capacity percentage
|
||||
- `BATTERY_RUNTIME`: OID for runtime remaining in minutes
|
||||
- `thresholds`: When to trigger shutdown
|
||||
- `battery`: Battery percentage threshold (default: 60%)
|
||||
- `runtime`: Runtime minutes threshold (default: 20 minutes)
|
||||
- `groups`: Array of group IDs this UPS belongs to
|
||||
- `groups`: Array of group configurations
|
||||
- `id`: Unique identifier for the group
|
||||
- `name`: Friendly name for the group
|
||||
- `mode`: Group operating mode ('redundant' or 'nonRedundant')
|
||||
- `description`: Optional description of the group
|
||||
|
||||
### Group Modes
|
||||
|
||||
- **Redundant Mode**: The system will only initiate shutdown if ALL UPS devices in the group are in critical condition (below threshold). This is ideal for redundant power setups where one UPS can keep systems running.
|
||||
|
||||
- **Non-Redundant Mode**: The system will initiate shutdown if ANY UPS device in the group is in critical condition. This is useful for scenarios where all UPS devices must be operational for the system to function properly.
|
||||
|
||||
## Setup as a Service
|
||||
|
||||
|
160
test/test.logger.ts
Normal file
160
test/test.logger.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { Logger } from '../ts/logger.js';
|
||||
|
||||
// Create a Logger instance for testing
|
||||
const logger = new Logger();
|
||||
|
||||
tap.test('should create a logger instance', async () => {
|
||||
expect(logger instanceof Logger).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should log messages with different log levels', async () => {
|
||||
// We're not testing console output directly, just ensuring no errors
|
||||
logger.log('Regular log message');
|
||||
logger.error('Error message');
|
||||
logger.warn('Warning message');
|
||||
logger.success('Success message');
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should create a logbox with title, content, and end', async () => {
|
||||
// Just ensuring no errors occur
|
||||
logger.logBoxTitle('Test Box', 40);
|
||||
logger.logBoxLine('This is a test line');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should handle width persistence between logbox calls', async () => {
|
||||
logger.logBoxTitle('Width Test', 45);
|
||||
|
||||
// These should use the width from the title
|
||||
logger.logBoxLine('Line 1');
|
||||
logger.logBoxLine('Line 2');
|
||||
logger.logBoxEnd();
|
||||
|
||||
let errorThrown = false;
|
||||
|
||||
try {
|
||||
// This should work fine after the reset in logBoxEnd
|
||||
logger.logBoxTitle('New Box', 30);
|
||||
logger.logBoxLine('New line');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
}
|
||||
|
||||
expect(errorThrown).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should use default width when no width is specified', async () => {
|
||||
// This should automatically use the default width instead of throwing
|
||||
let errorThrown = false;
|
||||
|
||||
try {
|
||||
logger.logBoxLine('This should use default width');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
}
|
||||
|
||||
// Verify no error was thrown
|
||||
expect(errorThrown).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should create a complete logbox in one call', async () => {
|
||||
// Just ensuring no errors occur
|
||||
logger.logBox('Complete Box', [
|
||||
'Line 1',
|
||||
'Line 2',
|
||||
'Line 3'
|
||||
], 40);
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should handle content that exceeds box width', async () => {
|
||||
// Just ensuring no errors occur when content is too long
|
||||
logger.logBox('Truncation Test', [
|
||||
'This line is way too long and should be truncated because it exceeds the available space'
|
||||
], 30);
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should create dividers with custom characters', async () => {
|
||||
// Just ensuring no errors occur
|
||||
logger.logDivider(30);
|
||||
logger.logDivider(20, '*');
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should create divider with default width', async () => {
|
||||
// This should use the default width
|
||||
logger.logDivider(undefined, '-');
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('Logger Demo', async () => {
|
||||
console.log('\n=== LOGGER DEMO ===\n');
|
||||
|
||||
// Basic logging
|
||||
logger.log('Regular log message');
|
||||
logger.error('Error message');
|
||||
logger.warn('Warning message');
|
||||
logger.success('Success message');
|
||||
|
||||
// Logbox with title, content lines, and end
|
||||
logger.logBoxTitle('Configuration Loaded', 50);
|
||||
logger.logBoxLine('SNMP Settings:');
|
||||
logger.logBoxLine(' Host: 127.0.0.1');
|
||||
logger.logBoxLine(' Port: 161');
|
||||
logger.logBoxLine(' Version: 1');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Complete logbox in one call
|
||||
logger.logBox('UPS Status', [
|
||||
'Power Status: onBattery',
|
||||
'Battery Capacity: 75%',
|
||||
'Runtime Remaining: 30 minutes'
|
||||
], 45);
|
||||
|
||||
// Logbox with content that's too long for the width
|
||||
logger.logBox('Truncation Example', [
|
||||
'This line is short enough to fit within the box width',
|
||||
'This line is way too long and will be truncated because it exceeds the available space for content within the logbox'
|
||||
], 40);
|
||||
|
||||
// Demonstrating logbox width being remembered
|
||||
logger.logBoxTitle('Width Persistence Example', 60);
|
||||
logger.logBoxLine('These lines use the width from the title');
|
||||
logger.logBoxLine('No need to specify the width again');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Demonstrating default width
|
||||
console.log('\nDefault Width Example:');
|
||||
logger.logBoxLine('This line uses the default width');
|
||||
logger.logBoxLine('Still using default width');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Divider example
|
||||
logger.log('\nDivider example:');
|
||||
logger.logDivider(30);
|
||||
logger.logDivider(30, '*');
|
||||
logger.logDivider(undefined, '=');
|
||||
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
// Export the default tap object
|
||||
export default tap.start();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/nupst',
|
||||
version: '2.6.7',
|
||||
version: '3.0.0',
|
||||
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
|
||||
}
|
||||
|
692
ts/daemon.ts
692
ts/daemon.ts
@ -4,14 +4,19 @@ import { exec, execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { NupstSnmp } from './snmp/manager.js';
|
||||
import type { ISnmpConfig } from './snmp/types.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Configuration interface for the daemon
|
||||
* UPS configuration interface
|
||||
*/
|
||||
export interface INupstConfig {
|
||||
export interface IUpsConfig {
|
||||
/** Unique ID for the UPS */
|
||||
id: string;
|
||||
/** Friendly name for the UPS */
|
||||
name: string;
|
||||
/** SNMP configuration settings */
|
||||
snmp: ISnmpConfig;
|
||||
/** Threshold settings for initiating shutdown */
|
||||
@ -21,8 +26,58 @@ export interface INupstConfig {
|
||||
/** Shutdown when runtime below this minutes */
|
||||
runtime: number;
|
||||
};
|
||||
/** Group IDs this UPS belongs to */
|
||||
groups: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Group configuration interface
|
||||
*/
|
||||
export interface IGroupConfig {
|
||||
/** Unique ID for the group */
|
||||
id: string;
|
||||
/** Friendly name for the group */
|
||||
name: string;
|
||||
/** Group operation mode */
|
||||
mode: 'redundant' | 'nonRedundant';
|
||||
/** Optional description */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration interface for the daemon
|
||||
*/
|
||||
export interface INupstConfig {
|
||||
/** UPS devices configuration */
|
||||
upsDevices: IUpsConfig[];
|
||||
/** Groups configuration */
|
||||
groups: IGroupConfig[];
|
||||
/** Check interval in milliseconds */
|
||||
checkInterval: number;
|
||||
|
||||
// Legacy fields for backward compatibility
|
||||
/** SNMP configuration settings (legacy) */
|
||||
snmp?: ISnmpConfig;
|
||||
/** Threshold settings (legacy) */
|
||||
thresholds?: {
|
||||
/** Shutdown when battery below this percentage */
|
||||
battery: number;
|
||||
/** Shutdown when runtime below this minutes */
|
||||
runtime: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* UPS status tracking interface
|
||||
*/
|
||||
interface IUpsStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
powerStatus: 'online' | 'onBattery' | 'unknown';
|
||||
batteryCapacity: number;
|
||||
batteryRuntime: number;
|
||||
lastStatusChange: number;
|
||||
lastCheckTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,32 +90,41 @@ export class NupstDaemon {
|
||||
|
||||
/** Default configuration */
|
||||
private readonly DEFAULT_CONFIG: INupstConfig = {
|
||||
snmp: {
|
||||
host: '127.0.0.1',
|
||||
port: 161,
|
||||
community: 'public',
|
||||
version: 1,
|
||||
timeout: 5000,
|
||||
// SNMPv3 defaults (used only if version === 3)
|
||||
securityLevel: 'authPriv',
|
||||
username: '',
|
||||
authProtocol: 'SHA',
|
||||
authKey: '',
|
||||
privProtocol: 'AES',
|
||||
privKey: '',
|
||||
// UPS model for OID selection
|
||||
upsModel: 'cyberpower'
|
||||
},
|
||||
thresholds: {
|
||||
battery: 60, // Shutdown when battery below 60%
|
||||
runtime: 20, // Shutdown when runtime below 20 minutes
|
||||
},
|
||||
upsDevices: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: {
|
||||
host: '127.0.0.1',
|
||||
port: 161,
|
||||
community: 'public',
|
||||
version: 1,
|
||||
timeout: 5000,
|
||||
// SNMPv3 defaults (used only if version === 3)
|
||||
securityLevel: 'authPriv',
|
||||
username: '',
|
||||
authProtocol: 'SHA',
|
||||
authKey: '',
|
||||
privProtocol: 'AES',
|
||||
privKey: '',
|
||||
// UPS model for OID selection
|
||||
upsModel: 'cyberpower'
|
||||
},
|
||||
thresholds: {
|
||||
battery: 60, // Shutdown when battery below 60%
|
||||
runtime: 20, // Shutdown when runtime below 20 minutes
|
||||
},
|
||||
groups: []
|
||||
}
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: 30000, // Check every 30 seconds
|
||||
};
|
||||
|
||||
private config: INupstConfig;
|
||||
private snmp: NupstSnmp;
|
||||
private isRunning: boolean = false;
|
||||
private upsStatus: Map<string, IUpsStatus> = new Map();
|
||||
|
||||
/**
|
||||
* Create a new daemon instance with the given SNMP manager
|
||||
@ -86,10 +150,36 @@ export class NupstDaemon {
|
||||
|
||||
// Read and parse config
|
||||
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
|
||||
this.config = JSON.parse(configData);
|
||||
const parsedConfig = JSON.parse(configData);
|
||||
|
||||
// Handle legacy configuration format
|
||||
if (!parsedConfig.upsDevices && parsedConfig.snmp) {
|
||||
// Convert legacy format to new format
|
||||
this.config = {
|
||||
upsDevices: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: parsedConfig.snmp,
|
||||
thresholds: parsedConfig.thresholds,
|
||||
groups: []
|
||||
}
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: parsedConfig.checkInterval
|
||||
};
|
||||
|
||||
logger.log('Legacy configuration format detected. Converting to multi-UPS format.');
|
||||
|
||||
// Save the new format
|
||||
await this.saveConfig(this.config);
|
||||
} else {
|
||||
this.config = parsedConfig;
|
||||
}
|
||||
|
||||
return this.config;
|
||||
} catch (error) {
|
||||
if (error.message.includes('No configuration found')) {
|
||||
if (error.message && error.message.includes('No configuration found')) {
|
||||
throw error; // Re-throw the no configuration error
|
||||
}
|
||||
|
||||
@ -125,7 +215,7 @@ export class NupstDaemon {
|
||||
console.error('┌─ Configuration Error ─────────────────────┐');
|
||||
console.error(`│ ${message}`);
|
||||
console.error('│ Please run \'nupst setup\' first to create a configuration.');
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
console.error('└───────────────────────────────────────────┘');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,11 +237,11 @@ export class NupstDaemon {
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
console.log('Daemon is already running');
|
||||
logger.log('Daemon is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Starting NUPST daemon...');
|
||||
logger.log('Starting NUPST daemon...');
|
||||
|
||||
try {
|
||||
// Load configuration - this will throw an error if config doesn't exist
|
||||
@ -165,45 +255,87 @@ export class NupstDaemon {
|
||||
this.snmp.getNupst().checkForUpdates().then(updateAvailable => {
|
||||
if (updateAvailable) {
|
||||
const updateStatus = this.snmp.getNupst().getUpdateStatus();
|
||||
console.log('┌─ Update Available ───────────────────────┐');
|
||||
console.log(`│ Current Version: ${updateStatus.currentVersion}`);
|
||||
console.log(`│ Latest Version: ${updateStatus.latestVersion}`);
|
||||
console.log('│ Run "sudo nupst update" to update');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Update Available', boxWidth);
|
||||
logger.logBoxLine(`Current Version: ${updateStatus.currentVersion}`);
|
||||
logger.logBoxLine(`Latest Version: ${updateStatus.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}).catch(() => {}); // Ignore errors checking for updates
|
||||
|
||||
// Initialize UPS status tracking
|
||||
this.initializeUpsStatus();
|
||||
|
||||
// Start UPS monitoring
|
||||
this.isRunning = true;
|
||||
await this.monitor();
|
||||
} catch (error) {
|
||||
this.isRunning = false;
|
||||
console.error(`Daemon failed to start: ${error.message}`);
|
||||
logger.error(`Daemon failed to start: ${error.message}`);
|
||||
process.exit(1); // Exit with error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize UPS status tracking for all UPS devices
|
||||
*/
|
||||
private initializeUpsStatus(): void {
|
||||
this.upsStatus.clear();
|
||||
|
||||
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
||||
for (const ups of this.config.upsDevices) {
|
||||
this.upsStatus.set(ups.id, {
|
||||
id: ups.id,
|
||||
name: ups.name,
|
||||
powerStatus: 'unknown',
|
||||
batteryCapacity: 100,
|
||||
batteryRuntime: 999, // High value as default
|
||||
lastStatusChange: Date.now(),
|
||||
lastCheckTime: 0
|
||||
});
|
||||
}
|
||||
|
||||
logger.log(`Initialized status tracking for ${this.config.upsDevices.length} UPS devices`);
|
||||
} else {
|
||||
logger.error('No UPS devices found in configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the loaded configuration settings
|
||||
*/
|
||||
private logConfigLoaded(): void {
|
||||
console.log('┌─ Configuration Loaded ─────────────────────┐');
|
||||
console.log('│ SNMP Settings:');
|
||||
console.log(`│ Host: ${this.config.snmp.host}`);
|
||||
console.log(`│ Port: ${this.config.snmp.port}`);
|
||||
console.log(`│ Version: ${this.config.snmp.version}`);
|
||||
console.log('│ Thresholds:');
|
||||
console.log(`│ Battery: ${this.config.thresholds.battery}%`);
|
||||
console.log(`│ Runtime: ${this.config.thresholds.runtime} minutes`);
|
||||
console.log(`│ Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('Configuration Loaded', boxWidth);
|
||||
|
||||
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.logBoxEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the monitoring daemon
|
||||
*/
|
||||
public stop(): void {
|
||||
console.log('Stopping NUPST daemon...');
|
||||
logger.log('Stopping NUPST daemon...');
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
@ -211,81 +343,241 @@ export class NupstDaemon {
|
||||
* Monitor the UPS status and trigger shutdown when necessary
|
||||
*/
|
||||
private async monitor(): Promise<void> {
|
||||
console.log('Starting UPS monitoring...');
|
||||
logger.log('Starting UPS monitoring...');
|
||||
|
||||
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
||||
logger.error('No UPS devices found in configuration. Monitoring stopped.');
|
||||
this.isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let lastStatus: 'online' | 'onBattery' | 'unknown' = 'unknown';
|
||||
let lastLogTime = 0; // Track when we last logged status
|
||||
const LOG_INTERVAL = 5 * 60 * 1000; // Log at least every 5 minutes (300000ms)
|
||||
|
||||
// Monitor continuously
|
||||
while (this.isRunning) {
|
||||
try {
|
||||
const status = await this.snmp.getUpsStatus(this.config.snmp);
|
||||
const currentTime = Date.now();
|
||||
const shouldLogStatus = (currentTime - lastLogTime) >= LOG_INTERVAL;
|
||||
// Check all UPS devices
|
||||
await this.checkAllUpsDevices();
|
||||
|
||||
// Log status changes
|
||||
if (status.powerStatus !== lastStatus) {
|
||||
console.log('┌──────────────────────────────────────────┐');
|
||||
console.log(`│ Power status changed: ${lastStatus} → ${status.powerStatus}`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
lastStatus = status.powerStatus;
|
||||
lastLogTime = currentTime; // Reset log timer when status changes
|
||||
}
|
||||
// Log status periodically (at least every 5 minutes)
|
||||
else if (shouldLogStatus) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log('┌──────────────────────────────────────────┐');
|
||||
console.log(`│ [${timestamp}] Periodic Status Update`);
|
||||
console.log(`│ Power Status: ${status.powerStatus}`);
|
||||
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
// Log periodic status update
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastLogTime >= LOG_INTERVAL) {
|
||||
this.logAllUpsStatus();
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
|
||||
// Handle battery power status
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
await this.handleOnBatteryStatus(status);
|
||||
}
|
||||
// Check if shutdown is required based on group configurations
|
||||
await this.evaluateGroupShutdownConditions();
|
||||
|
||||
// Wait before next check
|
||||
await this.sleep(this.config.checkInterval);
|
||||
} catch (error) {
|
||||
console.error('Error during UPS monitoring:', error);
|
||||
logger.error(`Error during UPS monitoring: ${error.message}`);
|
||||
await this.sleep(this.config.checkInterval);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('UPS monitoring stopped');
|
||||
logger.log('UPS monitoring stopped');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle UPS status when running on battery
|
||||
* Check status of all UPS devices
|
||||
*/
|
||||
private async handleOnBatteryStatus(status: {
|
||||
powerStatus: string,
|
||||
batteryCapacity: number,
|
||||
batteryRuntime: number
|
||||
}): Promise<void> {
|
||||
console.log('┌─ UPS Status ───────────────────────────────┐');
|
||||
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min │`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
private async checkAllUpsDevices(): Promise<void> {
|
||||
for (const ups of this.config.upsDevices) {
|
||||
try {
|
||||
const upsStatus = this.upsStatus.get(ups.id);
|
||||
if (!upsStatus) {
|
||||
// Initialize status for this UPS if not exists
|
||||
this.upsStatus.set(ups.id, {
|
||||
id: ups.id,
|
||||
name: ups.name,
|
||||
powerStatus: 'unknown',
|
||||
batteryCapacity: 100,
|
||||
batteryRuntime: 999,
|
||||
lastStatusChange: Date.now(),
|
||||
lastCheckTime: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Check UPS status
|
||||
const status = await this.snmp.getUpsStatus(ups.snmp);
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Get the current status from the map
|
||||
const currentStatus = this.upsStatus.get(ups.id);
|
||||
|
||||
// Update status with new values
|
||||
const updatedStatus = {
|
||||
...currentStatus,
|
||||
powerStatus: status.powerStatus,
|
||||
batteryCapacity: status.batteryCapacity,
|
||||
batteryRuntime: status.batteryRuntime,
|
||||
lastCheckTime: currentTime
|
||||
};
|
||||
|
||||
// Check if power status changed
|
||||
if (currentStatus.powerStatus !== status.powerStatus) {
|
||||
logger.logBoxTitle(`Power Status Change: ${ups.name}`, 50);
|
||||
logger.logBoxLine(`Status changed: ${currentStatus.powerStatus} → ${status.powerStatus}`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
updatedStatus.lastStatusChange = currentTime;
|
||||
}
|
||||
|
||||
// Update the status in the map
|
||||
this.upsStatus.set(ups.id, updatedStatus);
|
||||
} catch (error) {
|
||||
logger.error(`Error checking UPS ${ups.name} (${ups.id}): ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log status of all UPS devices
|
||||
*/
|
||||
private logAllUpsStatus(): void {
|
||||
const timestamp = new Date().toISOString();
|
||||
const boxWidth = 60;
|
||||
logger.logBoxTitle('Periodic Status Update', boxWidth);
|
||||
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
||||
logger.logBoxLine('');
|
||||
|
||||
// Check battery threshold
|
||||
if (status.batteryCapacity < this.config.thresholds.battery) {
|
||||
console.log('⚠️ WARNING: Battery capacity below threshold');
|
||||
console.log(`Current: ${status.batteryCapacity}% | Threshold: ${this.config.thresholds.battery}%`);
|
||||
await this.initiateShutdown('Battery capacity below threshold');
|
||||
for (const [id, status] of this.upsStatus.entries()) {
|
||||
logger.logBoxLine(`UPS: ${status.name} (${id})`);
|
||||
logger.logBoxLine(` Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(` Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
|
||||
logger.logBoxLine('');
|
||||
}
|
||||
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate if shutdown is required based on group configurations
|
||||
*/
|
||||
private async evaluateGroupShutdownConditions(): Promise<void> {
|
||||
if (!this.config.groups || this.config.groups.length === 0) {
|
||||
// No groups defined, check individual UPS conditions
|
||||
for (const [id, status] of this.upsStatus.entries()) {
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
// Find the UPS config
|
||||
const ups = this.config.upsDevices.find(u => u.id === id);
|
||||
if (ups) {
|
||||
await this.evaluateUpsShutdownCondition(ups, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check runtime threshold
|
||||
if (status.batteryRuntime < this.config.thresholds.runtime) {
|
||||
console.log('⚠️ WARNING: Runtime below threshold');
|
||||
console.log(`Current: ${status.batteryRuntime} min | Threshold: ${this.config.thresholds.runtime} min`);
|
||||
await this.initiateShutdown('Runtime below threshold');
|
||||
// Evaluate each group
|
||||
for (const group of this.config.groups) {
|
||||
// Find all UPS devices in this group
|
||||
const upsDevicesInGroup = this.config.upsDevices.filter(ups =>
|
||||
ups.groups && ups.groups.includes(group.id)
|
||||
);
|
||||
|
||||
if (upsDevicesInGroup.length === 0) {
|
||||
// No UPS devices in this group
|
||||
continue;
|
||||
}
|
||||
|
||||
if (group.mode === 'redundant') {
|
||||
// Redundant mode: only shutdown if ALL UPS devices in the group are in critical condition
|
||||
await this.evaluateRedundantGroup(group, upsDevicesInGroup);
|
||||
} else {
|
||||
// Non-redundant mode: shutdown if ANY UPS device in the group is in critical condition
|
||||
await this.evaluateNonRedundantGroup(group, upsDevicesInGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a redundant group for shutdown conditions
|
||||
* In redundant mode, we only shut down if ALL UPS devices are in critical condition
|
||||
*/
|
||||
private async evaluateRedundantGroup(group: IGroupConfig, upsDevices: IUpsConfig[]): Promise<void> {
|
||||
// Count UPS devices on battery and in critical condition
|
||||
let upsOnBattery = 0;
|
||||
let upsInCriticalCondition = 0;
|
||||
|
||||
for (const ups of upsDevices) {
|
||||
const status = this.upsStatus.get(ups.id);
|
||||
if (!status) continue;
|
||||
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
upsOnBattery++;
|
||||
|
||||
// Check if this UPS is in critical condition
|
||||
if (status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime) {
|
||||
upsInCriticalCondition++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All UPS devices must be online for a redundant group to be considered healthy
|
||||
const allUpsCount = upsDevices.length;
|
||||
|
||||
// If all UPS are on battery and in critical condition, shutdown
|
||||
if (upsOnBattery === allUpsCount && upsInCriticalCondition === allUpsCount) {
|
||||
logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50);
|
||||
logger.logBoxLine(`Mode: Redundant`);
|
||||
logger.logBoxLine(`All ${allUpsCount} UPS devices in critical condition`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
await this.initiateShutdown(`All UPS devices in redundant group "${group.name}" in critical condition`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a non-redundant group for shutdown conditions
|
||||
* In non-redundant mode, we shut down if ANY UPS device is in critical condition
|
||||
*/
|
||||
private async evaluateNonRedundantGroup(group: IGroupConfig, upsDevices: IUpsConfig[]): Promise<void> {
|
||||
for (const ups of upsDevices) {
|
||||
const status = this.upsStatus.get(ups.id);
|
||||
if (!status) continue;
|
||||
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
// Check if this UPS is in critical condition
|
||||
if (status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime) {
|
||||
logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50);
|
||||
logger.logBoxLine(`Mode: Non-Redundant`);
|
||||
logger.logBoxLine(`UPS ${ups.name} in critical condition`);
|
||||
logger.logBoxLine(`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`);
|
||||
logger.logBoxLine(`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
await this.initiateShutdown(`UPS "${ups.name}" in non-redundant group "${group.name}" in critical condition`);
|
||||
return; // Exit after initiating shutdown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate an individual UPS for shutdown conditions
|
||||
*/
|
||||
private async evaluateUpsShutdownCondition(ups: IUpsConfig, status: IUpsStatus): Promise<void> {
|
||||
// Only evaluate UPS devices not in any group
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check threshold conditions
|
||||
if (status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime) {
|
||||
logger.logBoxTitle(`UPS Shutdown Required: ${ups.name}`, 50);
|
||||
logger.logBoxLine(`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`);
|
||||
logger.logBoxLine(`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
await this.initiateShutdown(`UPS "${ups.name}" battery or runtime below threshold`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -293,7 +585,7 @@ export class NupstDaemon {
|
||||
* @param reason Reason for shutdown
|
||||
*/
|
||||
public async initiateShutdown(reason: string): Promise<void> {
|
||||
console.log(`Initiating system shutdown due to: ${reason}`);
|
||||
logger.log(`Initiating system shutdown due to: ${reason}`);
|
||||
|
||||
// Set a longer delay for shutdown to allow VMs and services to close
|
||||
const shutdownDelayMinutes = 5;
|
||||
@ -312,7 +604,7 @@ export class NupstDaemon {
|
||||
try {
|
||||
if (fs.existsSync(path)) {
|
||||
shutdownCmd = path;
|
||||
console.log(`Found shutdown command at: ${shutdownCmd}`);
|
||||
logger.log(`Found shutdown command at: ${shutdownCmd}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
@ -322,32 +614,32 @@ export class NupstDaemon {
|
||||
|
||||
if (shutdownCmd) {
|
||||
// Execute shutdown command with delay to allow for VM graceful shutdown
|
||||
console.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`);
|
||||
logger.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`);
|
||||
const { stdout } = await execFileAsync(shutdownCmd, [
|
||||
'-h',
|
||||
`+${shutdownDelayMinutes}`,
|
||||
`UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`
|
||||
]);
|
||||
console.log('Shutdown initiated:', stdout);
|
||||
console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
|
||||
logger.log(`Shutdown initiated: ${stdout}`);
|
||||
logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
|
||||
} else {
|
||||
// Try using the PATH to find shutdown
|
||||
try {
|
||||
console.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`, {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
console.log('Shutdown initiated:', stdout);
|
||||
logger.log(`Shutdown initiated: ${stdout}`);
|
||||
} catch (e) {
|
||||
throw new Error(`Shutdown command not found: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor UPS during shutdown and force immediate shutdown if battery gets too low
|
||||
console.log('Monitoring UPS during shutdown process...');
|
||||
logger.log('Monitoring UPS during shutdown process...');
|
||||
await this.monitorDuringShutdown();
|
||||
} catch (error) {
|
||||
console.error('Failed to initiate shutdown:', error);
|
||||
logger.error(`Failed to initiate shutdown: ${error}`);
|
||||
|
||||
// Try alternative shutdown methods
|
||||
const alternatives = [
|
||||
@ -376,30 +668,30 @@ export class NupstDaemon {
|
||||
}
|
||||
|
||||
if (cmdPath) {
|
||||
console.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
|
||||
logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
|
||||
await execFileAsync(cmdPath, alt.args);
|
||||
return; // Exit if successful
|
||||
} else {
|
||||
// Try using PATH environment
|
||||
console.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`);
|
||||
logger.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`);
|
||||
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
return; // Exit if successful
|
||||
}
|
||||
} catch (altError) {
|
||||
console.error(`Alternative method ${alt.cmd} failed:`, altError);
|
||||
logger.error(`Alternative method ${alt.cmd} failed: ${altError}`);
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
console.error('All shutdown methods failed');
|
||||
logger.error('All shutdown methods failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor UPS during system shutdown
|
||||
* Force immediate shutdown if battery gets critically low
|
||||
* Force immediate shutdown if any UPS gets critically low
|
||||
*/
|
||||
private async monitorDuringShutdown(): Promise<void> {
|
||||
const EMERGENCY_RUNTIME_THRESHOLD = 5; // 5 minutes remaining is critical
|
||||
@ -407,112 +699,126 @@ export class NupstDaemon {
|
||||
const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring
|
||||
const startTime = Date.now();
|
||||
|
||||
console.log(`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`);
|
||||
logger.log(`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`);
|
||||
|
||||
// Continue monitoring until max monitoring time is reached
|
||||
while (Date.now() - startTime < MAX_MONITORING_TIME) {
|
||||
try {
|
||||
console.log('Checking UPS status during shutdown...');
|
||||
const status = await this.snmp.getUpsStatus(this.config.snmp);
|
||||
logger.log('Checking UPS status during shutdown...');
|
||||
|
||||
console.log(`Current battery: ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`);
|
||||
|
||||
// If battery runtime gets critically low, force immediate shutdown
|
||||
if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) {
|
||||
console.log('┌─ EMERGENCY SHUTDOWN ─────────────────────┐');
|
||||
console.log(`│ Battery runtime critically low: ${status.batteryRuntime} minutes`);
|
||||
console.log('│ Forcing immediate shutdown!');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
|
||||
// Check all UPS devices
|
||||
for (const ups of this.config.upsDevices) {
|
||||
try {
|
||||
// Find shutdown command in common system paths
|
||||
const shutdownPaths = [
|
||||
'/sbin/shutdown',
|
||||
'/usr/sbin/shutdown',
|
||||
'/bin/shutdown',
|
||||
'/usr/bin/shutdown'
|
||||
];
|
||||
const status = await this.snmp.getUpsStatus(ups.snmp);
|
||||
|
||||
let shutdownCmd = '';
|
||||
for (const path of shutdownPaths) {
|
||||
if (fs.existsSync(path)) {
|
||||
shutdownCmd = path;
|
||||
console.log(`Found shutdown command at: ${shutdownCmd}`);
|
||||
break;
|
||||
}
|
||||
logger.log(`UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`);
|
||||
|
||||
// If any UPS battery runtime gets critically low, force immediate shutdown
|
||||
if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) {
|
||||
logger.logBoxTitle('EMERGENCY SHUTDOWN', 50);
|
||||
logger.logBoxLine(`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`);
|
||||
logger.logBoxLine('Forcing immediate shutdown!');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Force immediate shutdown
|
||||
await this.forceImmediateShutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shutdownCmd) {
|
||||
console.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
|
||||
await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']);
|
||||
} else {
|
||||
// Try using the PATH to find shutdown
|
||||
console.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Emergency shutdown failed, trying alternative methods...');
|
||||
|
||||
// Try alternative shutdown methods in sequence
|
||||
const alternatives = [
|
||||
{ cmd: 'poweroff', args: ['--force'] },
|
||||
{ cmd: 'halt', args: ['-p'] },
|
||||
{ cmd: 'systemctl', args: ['poweroff'] }
|
||||
];
|
||||
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
// Check common paths
|
||||
const paths = [
|
||||
`/sbin/${alt.cmd}`,
|
||||
`/usr/sbin/${alt.cmd}`,
|
||||
`/bin/${alt.cmd}`,
|
||||
`/usr/bin/${alt.cmd}`
|
||||
];
|
||||
|
||||
let cmdPath = '';
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
cmdPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdPath) {
|
||||
console.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`);
|
||||
await execFileAsync(cmdPath, alt.args);
|
||||
return; // Exit if successful
|
||||
} else {
|
||||
// Try using PATH
|
||||
console.log(`Emergency: trying ${alt.cmd} via PATH`);
|
||||
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
||||
env: process.env
|
||||
});
|
||||
return; // Exit if successful
|
||||
}
|
||||
} catch (altError) {
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
console.error('All emergency shutdown methods failed');
|
||||
} catch (upsError) {
|
||||
logger.error(`Error checking UPS ${ups.name} during shutdown: ${upsError.message}`);
|
||||
}
|
||||
|
||||
// Stop monitoring after initiating emergency shutdown
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait before checking again
|
||||
await this.sleep(CHECK_INTERVAL);
|
||||
} catch (error) {
|
||||
console.error('Error monitoring UPS during shutdown:', error);
|
||||
logger.error(`Error monitoring UPS during shutdown: ${error.message}`);
|
||||
await this.sleep(CHECK_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('UPS monitoring during shutdown completed');
|
||||
logger.log('UPS monitoring during shutdown completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Force an immediate system shutdown
|
||||
*/
|
||||
private async forceImmediateShutdown(): Promise<void> {
|
||||
try {
|
||||
// Find shutdown command in common system paths
|
||||
const shutdownPaths = [
|
||||
'/sbin/shutdown',
|
||||
'/usr/sbin/shutdown',
|
||||
'/bin/shutdown',
|
||||
'/usr/bin/shutdown'
|
||||
];
|
||||
|
||||
let shutdownCmd = '';
|
||||
for (const path of shutdownPaths) {
|
||||
if (fs.existsSync(path)) {
|
||||
shutdownCmd = path;
|
||||
logger.log(`Found shutdown command at: ${shutdownCmd}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shutdownCmd) {
|
||||
logger.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
|
||||
await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']);
|
||||
} else {
|
||||
// Try using the PATH to find shutdown
|
||||
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Emergency shutdown failed, trying alternative methods...');
|
||||
|
||||
// Try alternative shutdown methods in sequence
|
||||
const alternatives = [
|
||||
{ cmd: 'poweroff', args: ['--force'] },
|
||||
{ cmd: 'halt', args: ['-p'] },
|
||||
{ cmd: 'systemctl', args: ['poweroff'] }
|
||||
];
|
||||
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
// Check common paths
|
||||
const paths = [
|
||||
`/sbin/${alt.cmd}`,
|
||||
`/usr/sbin/${alt.cmd}`,
|
||||
`/bin/${alt.cmd}`,
|
||||
`/usr/bin/${alt.cmd}`
|
||||
];
|
||||
|
||||
let cmdPath = '';
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
cmdPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdPath) {
|
||||
logger.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`);
|
||||
await execFileAsync(cmdPath, alt.args);
|
||||
return; // Exit if successful
|
||||
} else {
|
||||
// Try using PATH
|
||||
logger.log(`Emergency: trying ${alt.cmd} via PATH`);
|
||||
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
||||
env: process.env
|
||||
});
|
||||
return; // Exit if successful
|
||||
}
|
||||
} catch (altError) {
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
logger.error('All emergency shutdown methods failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
1
ts/helpers/index.ts
Normal file
1
ts/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './shortid.js';
|
22
ts/helpers/shortid.ts
Normal file
22
ts/helpers/shortid.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Generate a short unique ID of 6 alphanumeric characters
|
||||
* @returns A 6-character alphanumeric string
|
||||
*/
|
||||
export function shortId(): string {
|
||||
// Define the character set: a-z, A-Z, 0-9
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
|
||||
// Generate cryptographically secure random values
|
||||
const randomValues = new Uint8Array(6);
|
||||
crypto.getRandomValues(randomValues);
|
||||
|
||||
// Map each random value to a character in our set
|
||||
let result = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
// Use modulo to map the random byte to a character index
|
||||
const index = randomValues[i] % chars.length;
|
||||
result += chars[index];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { NupstCli } from './cli.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* Main entry point for NUPST
|
||||
@ -13,6 +14,6 @@ async function main() {
|
||||
|
||||
// Run the main function and handle any errors
|
||||
main().catch(error => {
|
||||
console.error('Error:', error);
|
||||
logger.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
147
ts/logger.ts
Normal file
147
ts/logger.ts
Normal file
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* A simple logger class that provides consistent formatting for log messages
|
||||
* including support for logboxes with title, lines, and closing
|
||||
*/
|
||||
export class Logger {
|
||||
private currentBoxWidth: number | null = null;
|
||||
private static instance: Logger;
|
||||
|
||||
/** Default width to use when no width is specified */
|
||||
private readonly DEFAULT_WIDTH = 60;
|
||||
|
||||
/**
|
||||
* Creates a new Logger instance
|
||||
*/
|
||||
constructor() {
|
||||
this.currentBoxWidth = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton logger instance
|
||||
* @returns The singleton logger instance
|
||||
*/
|
||||
public static getInstance(): Logger {
|
||||
if (!Logger.instance) {
|
||||
Logger.instance = new Logger();
|
||||
}
|
||||
return Logger.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message
|
||||
* @param message Message to log
|
||||
*/
|
||||
public log(message: string): void {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message
|
||||
* @param message Error message to log
|
||||
*/
|
||||
public error(message: string): void {
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning message with a warning emoji
|
||||
* @param message Warning message to log
|
||||
*/
|
||||
public warn(message: string): void {
|
||||
console.warn(`⚠️ ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a success message with a checkmark
|
||||
* @param message Success message to log
|
||||
*/
|
||||
public success(message: string): void {
|
||||
console.log(`✓ ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a logbox title and set the current box width
|
||||
* @param title Title of the logbox
|
||||
* @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH
|
||||
*/
|
||||
public logBoxTitle(title: string, width?: number): void {
|
||||
this.currentBoxWidth = width || this.DEFAULT_WIDTH;
|
||||
|
||||
// Create the title line with appropriate padding
|
||||
const paddedTitle = ` ${title} `;
|
||||
const remainingSpace = this.currentBoxWidth - 3 - paddedTitle.length;
|
||||
|
||||
// Title line: ┌─ Title ───┐
|
||||
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`;
|
||||
|
||||
console.log(titleLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a logbox line
|
||||
* @param content Content of the line
|
||||
* @param width Optional width override. If not provided, uses the current box width or DEFAULT_WIDTH.
|
||||
*/
|
||||
public logBoxLine(content: string, width?: number): void {
|
||||
if (!this.currentBoxWidth && !width) {
|
||||
// No current width and no width provided, use default width
|
||||
this.logBoxTitle('', this.DEFAULT_WIDTH);
|
||||
}
|
||||
|
||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||
|
||||
// Calculate the available space for content
|
||||
const availableSpace = boxWidth - 2; // Account for left and right borders
|
||||
|
||||
if (content.length <= availableSpace - 1) {
|
||||
// If content fits with at least one space for the right border stripe
|
||||
const padding = availableSpace - content.length - 1;
|
||||
console.log(`│ ${content}${' '.repeat(padding)}│`);
|
||||
} else {
|
||||
// Content is too long, let it flow out of boundaries.
|
||||
console.log(`│ ${content}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a logbox end
|
||||
* @param width Optional width override. If not provided, uses the current box width or DEFAULT_WIDTH.
|
||||
*/
|
||||
public logBoxEnd(width?: number): void {
|
||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||
|
||||
// Create the bottom border: └────────┘
|
||||
console.log(`└${'─'.repeat(boxWidth - 2)}┘`);
|
||||
|
||||
// Reset the current box width
|
||||
this.currentBoxWidth = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a complete logbox with title, content lines, and ending
|
||||
* @param title Title of the logbox
|
||||
* @param lines Array of content lines
|
||||
* @param width Width of the logbox, defaults to DEFAULT_WIDTH
|
||||
*/
|
||||
public logBox(title: string, lines: string[], width?: number): void {
|
||||
this.logBoxTitle(title, width || this.DEFAULT_WIDTH);
|
||||
|
||||
for (const line of lines) {
|
||||
this.logBoxLine(line);
|
||||
}
|
||||
|
||||
this.logBoxEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a divider line
|
||||
* @param width Width of the divider, defaults to DEFAULT_WIDTH
|
||||
* @param character Character to use for the divider (default: ─)
|
||||
*/
|
||||
public logDivider(width?: number, character: string = '─'): void {
|
||||
console.log(character.repeat(width || this.DEFAULT_WIDTH));
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance for easy use
|
||||
export const logger = Logger.getInstance();
|
32
ts/nupst.ts
32
ts/nupst.ts
@ -4,6 +4,7 @@ import { NupstSystemd } from './systemd.js';
|
||||
import { commitinfo } from './00_commitinfo_data.js';
|
||||
import { spawn } from 'child_process';
|
||||
import * as https from 'https';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* Main Nupst class that coordinates all components
|
||||
@ -70,7 +71,7 @@ export class Nupst {
|
||||
|
||||
return this.updateAvailable;
|
||||
} catch (error) {
|
||||
console.error(`Error checking for updates: ${error.message}`);
|
||||
logger.error(`Error checking for updates: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -162,28 +163,33 @@ export class Nupst {
|
||||
*/
|
||||
public logVersionInfo(checkForUpdates: boolean = true): void {
|
||||
const version = this.getVersion();
|
||||
console.log('┌─ NUPST Version ────────────────────────┐');
|
||||
console.log(`│ Current Version: ${version}`);
|
||||
const boxWidth = 45;
|
||||
|
||||
logger.logBoxTitle('NUPST Version', boxWidth);
|
||||
logger.logBoxLine(`Current Version: ${version}`);
|
||||
|
||||
if (this.updateAvailable && this.latestVersion) {
|
||||
console.log(`│ Update Available: ${this.latestVersion}`);
|
||||
console.log('│ Run "sudo nupst update" to update');
|
||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
logger.logBoxEnd();
|
||||
} else if (checkForUpdates) {
|
||||
console.log('│ Checking for updates...');
|
||||
logger.logBoxLine('Checking for updates...');
|
||||
|
||||
// We can't end the box yet since we're in an async operation
|
||||
this.checkForUpdates().then(updateAvailable => {
|
||||
if (updateAvailable) {
|
||||
console.log(`│ Update Available: ${this.latestVersion}`);
|
||||
console.log('│ Run "sudo nupst update" to update');
|
||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
} else {
|
||||
console.log('│ You are running the latest version');
|
||||
logger.logBoxLine('You are running the latest version');
|
||||
}
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
logger.logBoxEnd();
|
||||
}).catch(() => {
|
||||
console.log('│ Could not check for updates');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
logger.logBoxLine('Could not check for updates');
|
||||
logger.logBoxEnd();
|
||||
});
|
||||
} else {
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import * as dgram from 'dgram';
|
||||
import * as snmp from 'net-snmp';
|
||||
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js';
|
||||
import { UpsOidSets } from './oid-sets.js';
|
||||
|
185
ts/systemd.ts
185
ts/systemd.ts
@ -1,6 +1,7 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { NupstDaemon } from './daemon.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* Class for managing systemd service
|
||||
@ -13,7 +14,7 @@ export class NupstSystemd {
|
||||
|
||||
/** Template for the systemd service file */
|
||||
private readonly serviceTemplate = `[Unit]
|
||||
Description=Node.js UPS Shutdown Tool
|
||||
Description=Node.js UPS Shutdown Tool for Multiple UPS Devices
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@ -47,10 +48,11 @@ WantedBy=multi-user.target
|
||||
try {
|
||||
await fs.access(configPath);
|
||||
} catch (error) {
|
||||
console.error('┌─ Configuration Error ─────────────────────┐');
|
||||
console.error(`│ No configuration file found at ${configPath}`);
|
||||
console.error('│ Please run \'nupst setup\' first to create a configuration.');
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('Configuration Error', boxWidth);
|
||||
logger.logBoxLine(`No configuration file found at ${configPath}`);
|
||||
logger.logBoxLine("Please run 'nupst add' first to create a UPS configuration.");
|
||||
logger.logBoxEnd();
|
||||
throw new Error('Configuration not found');
|
||||
}
|
||||
}
|
||||
@ -66,23 +68,24 @@ WantedBy=multi-user.target
|
||||
|
||||
// Write the service file
|
||||
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
|
||||
console.log('┌─ Service Installation ─────────────────────┐');
|
||||
console.log(`│ Service file created at ${this.serviceFilePath}`);
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('Service Installation', boxWidth);
|
||||
logger.logBoxLine(`Service file created at ${this.serviceFilePath}`);
|
||||
|
||||
// Reload systemd daemon
|
||||
execSync('systemctl daemon-reload');
|
||||
console.log('│ Systemd daemon reloaded');
|
||||
logger.logBoxLine('Systemd daemon reloaded');
|
||||
|
||||
// Enable the service
|
||||
execSync('systemctl enable nupst.service');
|
||||
console.log('│ Service enabled to start on boot');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
logger.logBoxLine('Service enabled to start on boot');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
if (error.message === 'Configuration not found') {
|
||||
// Just rethrow the error as the message has already been displayed
|
||||
throw error;
|
||||
}
|
||||
console.error('Failed to install systemd service:', error);
|
||||
logger.error(`Failed to install systemd service: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -97,15 +100,16 @@ WantedBy=multi-user.target
|
||||
await this.checkConfigExists();
|
||||
|
||||
execSync('systemctl start nupst.service');
|
||||
console.log('┌─ Service Status ─────────────────────────┐');
|
||||
console.log('│ NUPST service started successfully');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
logger.logBoxLine('NUPST service started successfully');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
if (error.message === 'Configuration not found') {
|
||||
// Exit with error code since configuration is required
|
||||
process.exit(1);
|
||||
}
|
||||
console.error('Failed to start service:', error);
|
||||
logger.error(`Failed to start service: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -117,9 +121,9 @@ WantedBy=multi-user.target
|
||||
public async stop(): Promise<void> {
|
||||
try {
|
||||
execSync('systemctl stop nupst.service');
|
||||
console.log('NUPST service stopped');
|
||||
logger.success('NUPST service stopped');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop service:', error);
|
||||
logger.error(`Failed to stop service: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -132,9 +136,10 @@ WantedBy=multi-user.target
|
||||
try {
|
||||
// Enable debug mode if requested
|
||||
if (debugMode) {
|
||||
console.log('┌─ Debug Mode ─────────────────────────────┐');
|
||||
console.log('│ SNMP debugging enabled - detailed logs will be shown');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Debug Mode', boxWidth);
|
||||
logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
|
||||
logger.logBoxEnd();
|
||||
this.daemon.getNupstSnmp().enableDebug();
|
||||
}
|
||||
|
||||
@ -150,9 +155,9 @@ WantedBy=multi-user.target
|
||||
}
|
||||
|
||||
await this.displayServiceStatus();
|
||||
await this.displayUpsStatus();
|
||||
await this.displayAllUpsStatus();
|
||||
} catch (error) {
|
||||
console.error(`Failed to get status: ${error.message}`);
|
||||
logger.error(`Failed to get status: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,49 +168,115 @@ WantedBy=multi-user.target
|
||||
private async displayServiceStatus(): Promise<void> {
|
||||
try {
|
||||
const serviceStatus = execSync('systemctl status nupst.service').toString();
|
||||
console.log('┌─ Service Status ─────────────────────────┐');
|
||||
console.log(serviceStatus.split('\n').map(line => `│ ${line}`).join('\n'));
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
// Process each line of the status output
|
||||
serviceStatus.split('\n').forEach(line => {
|
||||
logger.logBoxLine(line);
|
||||
});
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
console.error('┌─ Service Status ─────────────────────────┐');
|
||||
console.error('│ Service is not running');
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
logger.logBoxLine('Service is not running');
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the UPS status
|
||||
* Display all UPS statuses
|
||||
* @private
|
||||
*/
|
||||
private async displayUpsStatus(): Promise<void> {
|
||||
private async displayAllUpsStatus(): Promise<void> {
|
||||
try {
|
||||
// Explicitly load the configuration first to ensure it's up-to-date
|
||||
await this.daemon.loadConfig();
|
||||
const config = this.daemon.getConfig();
|
||||
const snmp = this.daemon.getNupstSnmp();
|
||||
|
||||
// Create a test config with appropriate timeout, similar to the test command
|
||||
const snmpConfig = {
|
||||
...config.snmp,
|
||||
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for status check
|
||||
// Check if we have the new multi-UPS config format
|
||||
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||
logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`);
|
||||
|
||||
// Show status for each UPS
|
||||
for (const ups of config.upsDevices) {
|
||||
await this.displaySingleUpsStatus(ups, snmp);
|
||||
}
|
||||
} else if (config.snmp) {
|
||||
// Legacy single UPS configuration
|
||||
const legacyUps = {
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds,
|
||||
groups: []
|
||||
};
|
||||
|
||||
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||
} else {
|
||||
logger.error('No UPS devices found in configuration');
|
||||
}
|
||||
} catch (error) {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('UPS Status', boxWidth);
|
||||
logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display status of a single UPS
|
||||
* @param ups UPS configuration
|
||||
* @param snmp SNMP manager
|
||||
*/
|
||||
private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle(`Connecting to UPS: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(`ID: ${ups.id}`);
|
||||
logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`);
|
||||
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
// Get group names if available
|
||||
const config = this.daemon.getConfig();
|
||||
const groupNames = ups.groups.map(groupId => {
|
||||
const group = config.groups?.find(g => g.id === groupId);
|
||||
return group ? group.name : groupId;
|
||||
});
|
||||
logger.logBoxLine(`Groups: ${groupNames.join(', ')}`);
|
||||
}
|
||||
|
||||
logger.logBoxEnd();
|
||||
|
||||
try {
|
||||
// Create a test config with a short timeout
|
||||
const testConfig = {
|
||||
...ups.snmp,
|
||||
timeout: Math.min(ups.snmp.timeout, 10000) // Use at most 10 seconds for status check
|
||||
};
|
||||
|
||||
console.log('┌─ Connecting to UPS... ────────────────────┐');
|
||||
console.log(`│ Host: ${config.snmp.host}:${config.snmp.port}`);
|
||||
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const status = await snmp.getUpsStatus(testConfig);
|
||||
|
||||
const status = await snmp.getUpsStatus(snmpConfig);
|
||||
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`);
|
||||
|
||||
console.log('┌─ UPS Status ───────────────────────────────┐');
|
||||
console.log(`│ Power Status: ${status.powerStatus}`);
|
||||
console.log(`│ Battery Capacity: ${status.batteryCapacity}%`);
|
||||
console.log(`│ Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
// Show threshold status
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Thresholds:');
|
||||
logger.logBoxLine(` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
|
||||
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
|
||||
}`);
|
||||
logger.logBoxLine(` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
|
||||
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
|
||||
}`);
|
||||
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
console.error('┌─ UPS Status ───────────────────────────────┐');
|
||||
console.error(`│ Failed to retrieve UPS status: ${error.message}`);
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,10 +292,10 @@ WantedBy=multi-user.target
|
||||
|
||||
// Reload systemd daemon
|
||||
execSync('systemctl daemon-reload');
|
||||
console.log('Systemd daemon reloaded');
|
||||
console.log('NUPST service has been successfully uninstalled');
|
||||
logger.log('Systemd daemon reloaded');
|
||||
logger.success('NUPST service has been successfully uninstalled');
|
||||
} catch (error) {
|
||||
console.error('Failed to disable and uninstall service:', error);
|
||||
logger.error(`Failed to disable and uninstall service: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -235,11 +306,11 @@ WantedBy=multi-user.target
|
||||
*/
|
||||
private async stopService(): Promise<void> {
|
||||
try {
|
||||
console.log('Stopping NUPST service...');
|
||||
logger.log('Stopping NUPST service...');
|
||||
execSync('systemctl stop nupst.service');
|
||||
} catch (error) {
|
||||
// Service might not be running, that's okay
|
||||
console.log('Service was not running or could not be stopped');
|
||||
logger.log('Service was not running or could not be stopped');
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,10 +320,10 @@ WantedBy=multi-user.target
|
||||
*/
|
||||
private async disableService(): Promise<void> {
|
||||
try {
|
||||
console.log('Disabling NUPST service...');
|
||||
logger.log('Disabling NUPST service...');
|
||||
execSync('systemctl disable nupst.service');
|
||||
} catch (error) {
|
||||
console.log('Service was not enabled or could not be disabled');
|
||||
logger.log('Service was not enabled or could not be disabled');
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,11 +333,11 @@ WantedBy=multi-user.target
|
||||
*/
|
||||
private async removeServiceFile(): Promise<void> {
|
||||
if (await fs.stat(this.serviceFilePath).catch(() => null)) {
|
||||
console.log(`Removing service file ${this.serviceFilePath}...`);
|
||||
logger.log(`Removing service file ${this.serviceFilePath}...`);
|
||||
await fs.unlink(this.serviceFilePath);
|
||||
console.log('Service file removed');
|
||||
logger.log('Service file removed');
|
||||
} else {
|
||||
console.log('Service file did not exist');
|
||||
logger.log('Service file did not exist');
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user