Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
70c16fa0a6 | |||
7ef38cf961 | |||
fce5a9bafd | |||
8ee21ea92b |
16
changelog.md
16
changelog.md
@@ -1,5 +1,21 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-03-25 - 1.10.1 - fix(systemd/readme)
|
||||||
|
Improve README documentation and fix UPS status retrieval in systemd service
|
||||||
|
|
||||||
|
- Updated README features and installation instructions to clarify SNMP version support, UPS models, and configuration
|
||||||
|
- Modified default SNMP host to '192.168.1.100' and added 'upsModel' property in configuration examples
|
||||||
|
- Enhanced instructions for real-time log viewing and update process in README
|
||||||
|
- Fixed systemd.ts to use a test configuration with an appropriate timeout when fetching UPS status
|
||||||
|
|
||||||
|
## 2025-03-25 - 1.10.0 - feat(core)
|
||||||
|
Add update checking and version logging across startup components
|
||||||
|
|
||||||
|
- In daemon.ts, log version info on startup and check for updates in the background using npm registry response
|
||||||
|
- In nupst.ts, implement getVersion, checkForUpdates, getUpdateStatus, and compareVersions functions with update notifications
|
||||||
|
- Establish bidirectional reference between Nupst and NupstSnmp to support version logging
|
||||||
|
- Update systemd service status output to include version information
|
||||||
|
|
||||||
## 2025-03-25 - 1.9.0 - feat(cli)
|
## 2025-03-25 - 1.9.0 - feat(cli)
|
||||||
Add update command to CLI to update NUPST from repository and refresh the systemd service
|
Add update command to CLI to update NUPST from repository and refresh the systemd service
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/nupst",
|
"name": "@serve.zone/nupst",
|
||||||
"version": "1.9.0",
|
"version": "1.10.1",
|
||||||
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
|
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
75
readme.md
75
readme.md
@@ -4,10 +4,14 @@ NUPST is a command-line tool that monitors SNMP-enabled UPS devices and initiate
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Monitors UPS devices using SNMP
|
- Monitors UPS devices using SNMP (v1, v2c, and v3 supported)
|
||||||
- Automatic shutdown when battery level falls below threshold
|
- Automatic shutdown when battery level falls below threshold
|
||||||
- Automatic shutdown when runtime remaining falls below threshold
|
- Automatic shutdown when runtime remaining falls below threshold
|
||||||
|
- Supports multiple UPS brands (CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv)
|
||||||
- Simple systemd service integration
|
- Simple systemd service integration
|
||||||
|
- Regular status logging for monitoring
|
||||||
|
- Real-time log viewing with journalctl
|
||||||
|
- Version checking and automatic updates
|
||||||
- Self-contained - includes its own Node.js runtime
|
- Self-contained - includes its own Node.js runtime
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -66,12 +70,18 @@ Usage:
|
|||||||
nupst enable - Install and enable the systemd service (requires root)
|
nupst enable - Install and enable the systemd service (requires root)
|
||||||
nupst disable - Stop and uninstall the systemd service (requires root)
|
nupst disable - Stop and uninstall the systemd service (requires root)
|
||||||
nupst daemon-start - Start the daemon process directly
|
nupst daemon-start - Start the daemon process directly
|
||||||
nupst logs - Show logs of the systemd service
|
nupst logs - Show logs of the systemd service in real-time
|
||||||
nupst stop - Stop the systemd service
|
nupst stop - Stop the systemd service
|
||||||
nupst start - Start the systemd service
|
nupst start - Start the systemd service
|
||||||
nupst status - Show status of the systemd service and UPS status
|
nupst status - Show status of the systemd service and UPS status
|
||||||
nupst setup - Run the interactive setup to configure SNMP settings
|
nupst setup - Run the interactive setup to configure SNMP settings
|
||||||
|
nupst test - Test the current configuration by connecting to the UPS
|
||||||
|
nupst update - Update NUPST from repository and refresh systemd service (requires root)
|
||||||
nupst help - Show this help message
|
nupst help - Show this help message
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--debug, -d - Enable debug mode for detailed SNMP logging
|
||||||
|
(Example: nupst test --debug)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -93,11 +103,12 @@ Alternatively, you can manually edit the configuration file at `/etc/nupst/confi
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"snmp": {
|
"snmp": {
|
||||||
"host": "127.0.0.1",
|
"host": "192.168.1.100",
|
||||||
"port": 161,
|
"port": 161,
|
||||||
"community": "public",
|
"community": "public",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"timeout": 5000
|
"timeout": 5000,
|
||||||
|
"upsModel": "cyberpower"
|
||||||
},
|
},
|
||||||
"thresholds": {
|
"thresholds": {
|
||||||
"battery": 60,
|
"battery": 60,
|
||||||
@@ -112,6 +123,7 @@ Alternatively, you can manually edit the configuration file at `/etc/nupst/confi
|
|||||||
- `port`: SNMP port (default: 161)
|
- `port`: SNMP port (default: 161)
|
||||||
- `version`: SNMP version (1, 2, or 3)
|
- `version`: SNMP version (1, 2, or 3)
|
||||||
- `timeout`: Timeout in milliseconds (default: 5000)
|
- `timeout`: Timeout in milliseconds (default: 5000)
|
||||||
|
- `upsModel`: The UPS model ('cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', or 'custom')
|
||||||
- For SNMPv1/v2c:
|
- For SNMPv1/v2c:
|
||||||
- `community`: SNMP community string (default: public)
|
- `community`: SNMP community string (default: public)
|
||||||
- For SNMPv3:
|
- For SNMPv3:
|
||||||
@@ -121,6 +133,11 @@ Alternatively, you can manually edit the configuration file at `/etc/nupst/confi
|
|||||||
- `authKey`: Authentication password/key
|
- `authKey`: Authentication password/key
|
||||||
- `privProtocol`: Privacy/encryption protocol ('DES' or 'AES')
|
- `privProtocol`: Privacy/encryption protocol ('DES' or 'AES')
|
||||||
- `privKey`: Privacy password/key
|
- `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
|
- `thresholds`: When to trigger shutdown
|
||||||
- `battery`: Battery percentage threshold (default: 60%)
|
- `battery`: Battery percentage threshold (default: 60%)
|
||||||
- `runtime`: Runtime minutes threshold (default: 20 minutes)
|
- `runtime`: Runtime minutes threshold (default: 20 minutes)
|
||||||
@@ -141,6 +158,56 @@ To check the status:
|
|||||||
nupst status
|
nupst status
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To view logs in real-time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nupst logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating NUPST
|
||||||
|
|
||||||
|
NUPST checks for updates automatically and will notify you when an update is available. To update to the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nupst update
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Pull the latest changes from the git repository
|
||||||
|
2. Run the installation scripts
|
||||||
|
3. Refresh the systemd service configuration
|
||||||
|
4. Restart the service if it was running
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
NUPST was designed with security in mind:
|
||||||
|
|
||||||
|
### Minimal Dependencies
|
||||||
|
|
||||||
|
- **Zero Runtime NPM Dependencies**: NUPST is built without any external NPM packages to minimize the attack surface and avoid supply chain risks.
|
||||||
|
- **Self-contained Node.js**: NUPST ships with its own Node.js binary, isolated from the system's Node.js installation. This ensures:
|
||||||
|
- No dependency on system Node.js versions
|
||||||
|
- Zero external libraries that could become compromised
|
||||||
|
- Consistent, tested environment for execution
|
||||||
|
- Reduced risk of dependency-based attacks
|
||||||
|
|
||||||
|
### Implementation Security
|
||||||
|
|
||||||
|
- **Privilege Separation**: Only specific commands that require elevated permissions (`enable`, `disable`, `update`) check for root access; all other functionality runs with minimal privileges.
|
||||||
|
- **Limited Network Access**: NUPST only communicates with the UPS device over SNMP and contacts npmjs.org only to check for updates.
|
||||||
|
- **Secure SNMPv3 Support**: Supports encrypted authentication and privacy for secure communication with the UPS device.
|
||||||
|
- **Isolated Execution**: The application runs in its working directory (`/opt/nupst`) or specified installation location, minimizing the impact on the rest of the system.
|
||||||
|
|
||||||
|
### Installation Security
|
||||||
|
|
||||||
|
- The installation script can be reviewed before execution (`curl -sSL [url] | less`)
|
||||||
|
- All setup scripts download only verified versions and check integrity
|
||||||
|
- Installation is transparent and places files in standard locations (`/opt/nupst`, `/usr/local/bin`, `/etc/systemd/system`)
|
||||||
|
|
||||||
|
### Audit and Review
|
||||||
|
|
||||||
|
The codebase is small, focused, and designed to be easily auditable. All code is open source and available for review.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/nupst',
|
name: '@serve.zone/nupst',
|
||||||
version: '1.9.0',
|
version: '1.10.1',
|
||||||
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
|
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
|
||||||
}
|
}
|
||||||
|
15
ts/daemon.ts
15
ts/daemon.ts
@@ -152,6 +152,21 @@ export class NupstDaemon {
|
|||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
this.logConfigLoaded();
|
this.logConfigLoaded();
|
||||||
|
|
||||||
|
// Log version information
|
||||||
|
this.snmp.getNupst().logVersionInfo(false); // Don't check for updates immediately on startup
|
||||||
|
|
||||||
|
// Check for updates in the background
|
||||||
|
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('└──────────────────────────────────────────┘');
|
||||||
|
}
|
||||||
|
}).catch(() => {}); // Ignore errors checking for updates
|
||||||
|
|
||||||
// Start UPS monitoring
|
// Start UPS monitoring
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
await this.monitor();
|
await this.monitor();
|
||||||
|
146
ts/nupst.ts
146
ts/nupst.ts
@@ -1,6 +1,9 @@
|
|||||||
import { NupstSnmp } from './snmp.js';
|
import { NupstSnmp } from './snmp.js';
|
||||||
import { NupstDaemon } from './daemon.js';
|
import { NupstDaemon } from './daemon.js';
|
||||||
import { NupstSystemd } from './systemd.js';
|
import { NupstSystemd } from './systemd.js';
|
||||||
|
import { commitinfo } from './00_commitinfo_data.js';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import * as https from 'https';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Nupst class that coordinates all components
|
* Main Nupst class that coordinates all components
|
||||||
@@ -10,12 +13,15 @@ export class Nupst {
|
|||||||
private readonly snmp: NupstSnmp;
|
private readonly snmp: NupstSnmp;
|
||||||
private readonly daemon: NupstDaemon;
|
private readonly daemon: NupstDaemon;
|
||||||
private readonly systemd: NupstSystemd;
|
private readonly systemd: NupstSystemd;
|
||||||
|
private updateAvailable: boolean = false;
|
||||||
|
private latestVersion: string = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Nupst instance with all necessary components
|
* Create a new Nupst instance with all necessary components
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.snmp = new NupstSnmp();
|
this.snmp = new NupstSnmp();
|
||||||
|
this.snmp.setNupst(this); // Set up bidirectional reference
|
||||||
this.daemon = new NupstDaemon(this.snmp);
|
this.daemon = new NupstDaemon(this.snmp);
|
||||||
this.systemd = new NupstSystemd(this.daemon);
|
this.systemd = new NupstSystemd(this.daemon);
|
||||||
}
|
}
|
||||||
@@ -40,4 +46,144 @@ export class Nupst {
|
|||||||
public getSystemd(): NupstSystemd {
|
public getSystemd(): NupstSystemd {
|
||||||
return this.systemd;
|
return this.systemd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current version of NUPST
|
||||||
|
* @returns The current version string
|
||||||
|
*/
|
||||||
|
public getVersion(): string {
|
||||||
|
return commitinfo.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an update is available
|
||||||
|
* @returns Promise resolving to true if an update is available
|
||||||
|
*/
|
||||||
|
public async checkForUpdates(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const latestVersion = await this.getLatestVersion();
|
||||||
|
const currentVersion = this.getVersion();
|
||||||
|
|
||||||
|
// Compare versions
|
||||||
|
this.updateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
|
||||||
|
this.latestVersion = latestVersion;
|
||||||
|
|
||||||
|
return this.updateAvailable;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking for updates: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update status information
|
||||||
|
* @returns Object with update status information
|
||||||
|
*/
|
||||||
|
public getUpdateStatus(): {
|
||||||
|
currentVersion: string,
|
||||||
|
latestVersion: string,
|
||||||
|
updateAvailable: boolean
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
currentVersion: this.getVersion(),
|
||||||
|
latestVersion: this.latestVersion || this.getVersion(),
|
||||||
|
updateAvailable: this.updateAvailable
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest version from npm registry
|
||||||
|
* @returns Promise resolving to the latest version string
|
||||||
|
*/
|
||||||
|
private async getLatestVersion(): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: 'registry.npmjs.org',
|
||||||
|
path: '/@serve.zone/nupst',
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': `nupst/${this.getVersion()}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(data);
|
||||||
|
if (response['dist-tags'] && response['dist-tags'].latest) {
|
||||||
|
resolve(response['dist-tags'].latest);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Failed to parse version from npm registry response'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two semantic version strings
|
||||||
|
* @param versionA First version
|
||||||
|
* @param versionB Second version
|
||||||
|
* @returns -1 if versionA < versionB, 0 if equal, 1 if versionA > versionB
|
||||||
|
*/
|
||||||
|
private compareVersions(versionA: string, versionB: string): number {
|
||||||
|
const partsA = versionA.split('.').map(part => parseInt(part, 10));
|
||||||
|
const partsB = versionB.split('.').map(part => parseInt(part, 10));
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||||
|
const partA = i < partsA.length ? partsA[i] : 0;
|
||||||
|
const partB = i < partsB.length ? partsB[i] : 0;
|
||||||
|
|
||||||
|
if (partA > partB) return 1;
|
||||||
|
if (partA < partB) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Versions are equal
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the current version and update status
|
||||||
|
*/
|
||||||
|
public logVersionInfo(checkForUpdates: boolean = true): void {
|
||||||
|
const version = this.getVersion();
|
||||||
|
console.log('┌─ NUPST Version ────────────────────────┐');
|
||||||
|
console.log(`│ Current Version: ${version}`);
|
||||||
|
|
||||||
|
if (this.updateAvailable && this.latestVersion) {
|
||||||
|
console.log(`│ Update Available: ${this.latestVersion}`);
|
||||||
|
console.log('│ Run "sudo nupst update" to update');
|
||||||
|
} else if (checkForUpdates) {
|
||||||
|
console.log('│ Checking for updates...');
|
||||||
|
this.checkForUpdates().then(updateAvailable => {
|
||||||
|
if (updateAvailable) {
|
||||||
|
console.log(`│ Update Available: ${this.latestVersion}`);
|
||||||
|
console.log('│ Run "sudo nupst update" to update');
|
||||||
|
} else {
|
||||||
|
console.log('│ You are running the latest version');
|
||||||
|
}
|
||||||
|
console.log('└──────────────────────────────────────────┘');
|
||||||
|
}).catch(() => {
|
||||||
|
console.log('│ Could not check for updates');
|
||||||
|
console.log('└──────────────────────────────────────────┘');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('└──────────────────────────────────────────┘');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,8 @@ const execAsync = promisify(exec);
|
|||||||
export class NupstSnmp {
|
export class NupstSnmp {
|
||||||
// Active OID set
|
// Active OID set
|
||||||
private activeOIDs: OIDSet;
|
private activeOIDs: OIDSet;
|
||||||
|
// Reference to the parent Nupst instance
|
||||||
|
private nupst: any; // Type 'any' to avoid circular dependency
|
||||||
|
|
||||||
// Default SNMP configuration
|
// Default SNMP configuration
|
||||||
private readonly DEFAULT_CONFIG: SnmpConfig = {
|
private readonly DEFAULT_CONFIG: SnmpConfig = {
|
||||||
@@ -43,6 +45,21 @@ export class NupstSnmp {
|
|||||||
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
|
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set reference to the main Nupst instance
|
||||||
|
* @param nupst Reference to the main Nupst instance
|
||||||
|
*/
|
||||||
|
public setNupst(nupst: any): void {
|
||||||
|
this.nupst = nupst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get reference to the main Nupst instance
|
||||||
|
*/
|
||||||
|
public getNupst(): any {
|
||||||
|
return this.nupst;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set active OID set based on UPS model
|
* Set active OID set based on UPS model
|
||||||
* @param config SNMP configuration
|
* @param config SNMP configuration
|
||||||
|
@@ -129,6 +129,9 @@ WantedBy=multi-user.target
|
|||||||
*/
|
*/
|
||||||
public async getStatus(): Promise<void> {
|
public async getStatus(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
// Display version information
|
||||||
|
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
|
||||||
|
|
||||||
// Check if config exists first
|
// Check if config exists first
|
||||||
try {
|
try {
|
||||||
await this.checkConfigExists();
|
await this.checkConfigExists();
|
||||||
@@ -167,9 +170,16 @@ WantedBy=multi-user.target
|
|||||||
*/
|
*/
|
||||||
private async displayUpsStatus(): Promise<void> {
|
private async displayUpsStatus(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const upsStatus = await this.daemon.getConfig().snmp;
|
const config = this.daemon.getConfig();
|
||||||
const snmp = this.daemon.getNupstSnmp();
|
const snmp = this.daemon.getNupstSnmp();
|
||||||
const status = await snmp.getUpsStatus(upsStatus);
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = await snmp.getUpsStatus(snmpConfig);
|
||||||
|
|
||||||
console.log('┌─ UPS Status ───────────────────────────────┐');
|
console.log('┌─ UPS Status ───────────────────────────────┐');
|
||||||
console.log(`│ Power Status: ${status.powerStatus}`);
|
console.log(`│ Power Status: ${status.powerStatus}`);
|
||||||
|
Reference in New Issue
Block a user