Compare commits

...

4 Commits

8 changed files with 279 additions and 8 deletions

View File

@@ -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

View File

@@ -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": {

View File

@@ -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

View File

@@ -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'
} }

View File

@@ -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();

View File

@@ -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('└──────────────────────────────────────────┘');
}
}
} }

View File

@@ -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

View File

@@ -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}`);