Compare commits

...

20 Commits

Author SHA1 Message Date
2737fca294 2.5.1 2025-03-25 14:47:57 +00:00
896233914f fix(snmp): Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion. 2025-03-25 14:47:57 +00:00
5bb775b17d 2.5.0 2025-03-25 13:26:28 +00:00
ae8219acf7 feat(cli): Automatically restart running NUPST service after configuration changes in interactive setup 2025-03-25 13:26:27 +00:00
4ad383884c 2.4.8 2025-03-25 13:20:36 +00:00
65a9d1c798 fix(installer): Improve Git dependency handling and repository cloning in install.sh 2025-03-25 13:20:36 +00:00
f583e1466f 2.4.7 2025-03-25 13:17:28 +00:00
9d893a97b6 fix(readme): Update installation instructions to combine download and execution into a single command for clarity 2025-03-25 13:17:28 +00:00
aa52d5e9f6 2.4.6 2025-03-25 13:15:48 +00:00
623b7ee51f fix(installer): Improve installation instructions for interactive and non-interactive setups 2025-03-25 13:15:48 +00:00
897e86ad60 2.4.5 2025-03-25 13:12:38 +00:00
ed78db20e2 fix(install): Improve interactive terminal detection and update installation instructions 2025-03-25 13:12:38 +00:00
bd00dfe02c 2.4.4 2025-03-25 13:08:28 +00:00
55c040df82 fix(install): Improve interactive mode detection and non-interactive installation handling in install.sh 2025-03-25 13:08:28 +00:00
e68654a022 2.4.3 2025-03-25 12:57:12 +00:00
89a5d23d2f fix(readme): Update Quick Install command syntax in readme for auto-yes installation 2025-03-25 12:57:12 +00:00
f9aa1cfd2f 2.4.2 2025-03-25 11:49:50 +00:00
e47f316d0a fix(daemon): Refactor shutdown initiation logic in daemon by moving the initiateShutdown and monitorDuringShutdown methods from the SNMP manager to the daemon, and update calls accordingly 2025-03-25 11:49:50 +00:00
901127f784 2.4.1 2025-03-25 11:36:11 +00:00
dc4fd5afba fix(docs): Update readme with detailed legal and trademark guidance 2025-03-25 11:36:11 +00:00
9 changed files with 332 additions and 58 deletions

View File

@ -1,5 +1,67 @@
# Changelog
## 2025-03-25 - 2.5.1 - fix(snmp)
Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion.
- Updated Eaton UPS power status OID to '1.3.6.1.4.1.534.1.4.4.0' to correctly detect online/battery status.
- Added conversion for Eaton UPS battery runtime from seconds to minutes in SNMP manager.
## 2025-03-25 - 2.5.0 - feat(cli)
Automatically restart running NUPST service after configuration changes in interactive setup
- Added restartServiceIfRunning() to check and restart the service if it's active.
- Invoked the restart function post-setup to apply configuration changes immediately.
## 2025-03-25 - 2.4.8 - fix(installer)
Improve Git dependency handling and repository cloning in install.sh
- Add explicit check for git installation and prompt the user interactively if git is missing.
- Auto-install git when '-y' flag is provided in non-interactive mode.
- Ensure proper cloning of the repository when running the installer outside the repo.
## 2025-03-25 - 2.4.7 - fix(readme)
Update installation instructions to combine download and execution into a single command for clarity
- Method 1 now uses a unified one-line command to download and run the install script
## 2025-03-25 - 2.4.6 - fix(installer)
Improve installation instructions for interactive and non-interactive setups
- Changed install.sh to require explicit download of the install script and updated error messages for non-interactive modes
- Updated readme.md to include three distinct installation methods with clear command examples
## 2025-03-25 - 2.4.5 - fix(install)
Improve interactive terminal detection and update installation instructions
- Enhanced install.sh to better detect non-interactive environments and provide clearer guidance for both interactive and non-interactive installations
- Updated README.md quick install instructions to recommend process substitution and clarify auto-yes usage
## 2025-03-25 - 2.4.4 - fix(install)
Improve interactive mode detection and non-interactive installation handling in install.sh
- Detect and warn when running without a controlling terminal
- Attempt to use /dev/tty for user input when possible
- Update prompts and error messages for auto-installation of dependencies
- Clarify installation instructions in readme for interactive and non-interactive modes
## 2025-03-25 - 2.4.3 - fix(readme)
Update Quick Install command syntax in readme for auto-yes installation
- Changed installation command to use: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -c "bash -s -- -y"
## 2025-03-25 - 2.4.2 - fix(daemon)
Refactor shutdown initiation logic in daemon by moving the initiateShutdown and monitorDuringShutdown methods from the SNMP manager to the daemon, and update calls accordingly
- Moved initiateShutdown and monitorDuringShutdown to the daemon class for improved cohesion
- Updated references in the daemon to call its own shutdown method instead of the SNMP manager
- Removed redundant initiateShutdown method from the SNMP manager
## 2025-03-25 - 2.4.1 - fix(docs)
Update readme with detailed legal and trademark guidance
- Clarified legal section by adding trademark and company information
- Ensured users understand that licensing terms do not imply endorsement by the company
## 2025-03-25 - 2.4.0 - feat(installer)
Add auto-yes flag to installer and update installation documentation

View File

@ -50,11 +50,42 @@ fi
# Detect if script is being piped or run directly
PIPED=0
INTERACTIVE=1
if [ ! -t 0 ]; then
# Being piped, need to clone the repo
PIPED=1
fi
# Check if stdin is a terminal
if [ ! -t 0 ] || [ ! -t 1 ]; then
# Either stdin or stdout is not a terminal, check if -y was provided
if [ $AUTO_YES -ne 1 ]; then
echo "Script detected it's running in a non-interactive environment without -y flag."
echo "Attempting to find a controlling terminal for interactive prompts..."
# Try to use a controlling terminal for user input
if [ -t 1 ]; then
# Stdout is a terminal, use it
exec < /dev/tty 2>/dev/null || INTERACTIVE=0
else
# Try to find controlling terminal
exec < /dev/tty 2>/dev/null || INTERACTIVE=0
fi
if [ $INTERACTIVE -eq 0 ]; then
echo "ERROR: No controlling terminal available for interactive prompts."
echo "For interactive installation (RECOMMENDED):"
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh"
echo " sudo bash nupst-install.sh"
echo ""
echo "For non-interactive installation with automatic dependency installation:"
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y"
exit 1
else
echo "Interactive terminal found, continuing with prompts..."
fi
fi
fi
# Helper function to detect OS type
detect_os() {
if [ -f /etc/os-release ]; then
@ -131,29 +162,37 @@ install_git() {
INSTALL_DIR="/opt/nupst"
REPO_URL="https://code.foss.global/serve.zone/nupst.git"
if [ $PIPED -eq 1 ]; then
echo "Installing NUPST from remote repository..."
# Check if git is installed - needed for both piped and direct execution
if ! command -v git &> /dev/null; then
echo "Git is required but not installed."
# Check if git is installed
if ! command -v git &> /dev/null; then
echo "Git is required but not installed."
if [ $AUTO_YES -eq 1 ]; then
echo "Auto-installing git (-y flag provided)..."
install_git
elif [ $INTERACTIVE -eq 1 ]; then
# If interactive and no -y flag, ask the user
echo "Would you like to install git now? (y/N): "
read -r install_git_prompt
if [ $AUTO_YES -eq 1 ]; then
echo "Auto-installing git (-y flag provided)..."
if [[ "$install_git_prompt" =~ ^[Yy]$ ]]; then
install_git
else
read -p "Would you like to install git now? (y/N): " install_git_prompt
if [[ "$install_git_prompt" =~ ^[Yy]$ ]]; then
install_git
else
echo "Git installation skipped. Please install git manually and run the installer again."
echo "Alternatively, you can run the installer with -y flag to automatically install git:"
echo " sudo bash install.sh -y"
exit 1
fi
echo "Git installation skipped. Please install git manually and run the installer again."
echo "Alternatively, you can run the installer with -y flag to automatically install git:"
echo " sudo bash install.sh -y"
exit 1
fi
else
# Non-interactive mode without -y flag
echo "Error: Git is required but not installed."
echo "In non-interactive mode, use -y flag to auto-install dependencies:"
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y"
exit 1
fi
fi
if [ $PIPED -eq 1 ]; then
echo "Installing NUPST from remote repository..."
# Check if installation directory exists
if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then
@ -196,12 +235,47 @@ if [ $PIPED -eq 1 ]; then
# Set script directory to the cloned repo
SCRIPT_DIR="$INSTALL_DIR"
else
# Running directly from within the repo
# Running directly from within the repo or downloaded script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# When running from a downloaded script in a different location
# we need to clone the repository first
if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then
echo "Running installer from downloaded script outside repository."
echo "Will clone the repository to $INSTALL_DIR..."
# Create installation directory if needed
if [ -d "$INSTALL_DIR" ]; then
echo "Removing previous installation at $INSTALL_DIR..."
rm -rf "$INSTALL_DIR"
fi
mkdir -p "$INSTALL_DIR"
# Clone the repository
echo "Cloning NUPST repository to $INSTALL_DIR..."
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
if [ $? -ne 0 ]; then
echo "Failed to clone repository. Please check your internet connection."
exit 1
fi
# Update script directory to use the cloned repo
SCRIPT_DIR="$INSTALL_DIR"
fi
fi
# Run setup script
echo "Running setup script..."
if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then
echo "ERROR: Setup script not found at $SCRIPT_DIR/setup.sh"
echo "Current directory: $(pwd)"
echo "Script directory: $SCRIPT_DIR"
ls -la "$SCRIPT_DIR"
exit 1
fi
bash "$SCRIPT_DIR/setup.sh"
# Install globally

View File

@ -1,6 +1,6 @@
{
"name": "@serve.zone/nupst",
"version": "2.4.0",
"version": "2.5.1",
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
"main": "dist/index.js",
"bin": {

View File

@ -19,13 +19,21 @@ NUPST is a command-line tool that monitors SNMP-enabled UPS devices and initiate
### Quick Install (One-line command)
```bash
# Install directly without cloning the repository (requires root privileges)
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
# Method 1: Download and run (most reliable across all environments)
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh && sudo bash nupst-install.sh && rm nupst-install.sh
```
# Install with auto-yes for dependencies (will install git automatically if needed)
```bash
# Method 2: Pipe with automatic yes for dependencies (non-interactive)
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y
```
```bash
# Method 3: Process substitution (only on systems that support /dev/fd/)
# Note: This may fail on some systems with "No such file or directory" errors
sudo bash <(curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh)
```
### Direct from Git
```bash
@ -252,6 +260,21 @@ NUPST was designed with security in mind:
The codebase is small, focused, and designed to be easily auditable. All code is open source and available for review.
## License
## License and Legal Information
MIT
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/nupst',
version: '2.4.0',
version: '2.5.1',
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
}

View File

@ -533,6 +533,9 @@ Options:
// Test the connection if requested
await this.optionallyTestConnection(config, prompt);
// Check if service is running and restart it if needed
await this.restartServiceIfRunning();
console.log('\nSetup complete!');
await this.optionallyEnableService(prompt);
}
@ -833,6 +836,44 @@ Options:
}
}
/**
* Check if the systemd service is running and restart it if it is
* This is useful after configuration changes
*/
private async restartServiceIfRunning(): Promise<void> {
try {
// Check if the service is active
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isActive) {
// Service is running, restart it
console.log('┌─ Service Update ─────────────────────────┐');
console.log('│ Configuration has changed.');
console.log('│ Restarting NUPST service to apply changes...');
try {
if (process.getuid && process.getuid() === 0) {
// We have root access, restart directly
execSync('systemctl restart nupst.service');
console.log('│ Service restarted successfully.');
} else {
// No root access, show instructions
console.log('│ Please restart the service with:');
console.log('│ sudo systemctl restart nupst.service');
}
} catch (error) {
console.log(`│ Error restarting service: ${error.message}`);
console.log('│ You may need to restart the service manually:');
console.log('│ sudo systemctl restart nupst.service');
}
console.log('└──────────────────────────────────────────┘');
}
} catch (error) {
// Ignore errors checking service status
}
}
/**
* Optionally enable and start systemd service
* @param prompt Function to prompt for user input

View File

@ -1,7 +1,11 @@
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { NupstSnmp, type ISnmpConfig } from './snmp.js';
const execAsync = promisify(exec);
/**
* Configuration interface for the daemon
*/
@ -269,7 +273,7 @@ export class NupstDaemon {
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.snmp.initiateShutdown('Battery capacity below threshold');
await this.initiateShutdown('Battery capacity below threshold');
return;
}
@ -277,10 +281,90 @@ export class NupstDaemon {
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.snmp.initiateShutdown('Runtime below threshold');
await this.initiateShutdown('Runtime below threshold');
return;
}
}
/**
* Initiate system shutdown with UPS monitoring during shutdown
* @param reason Reason for shutdown
*/
public async initiateShutdown(reason: string): Promise<void> {
console.log(`Initiating system shutdown due to: ${reason}`);
// Set a longer delay for shutdown to allow VMs and services to close
const shutdownDelayMinutes = 5;
try {
// Execute shutdown command with delay to allow for VM graceful shutdown
const { stdout } = await execAsync(`shutdown -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`);
// Monitor UPS during shutdown and force immediate shutdown if battery gets too low
console.log('Monitoring UPS during shutdown process...');
await this.monitorDuringShutdown();
} catch (error) {
console.error('Failed to initiate shutdown:', error);
// Try a different method if first one fails
try {
console.log('Trying alternative shutdown method...');
await execAsync('poweroff --force');
} catch (innerError) {
console.error('All shutdown methods failed:', innerError);
}
}
}
/**
* Monitor UPS during system shutdown
* Force immediate shutdown if battery gets critically low
*/
private async monitorDuringShutdown(): Promise<void> {
const EMERGENCY_RUNTIME_THRESHOLD = 5; // 5 minutes remaining is critical
const CHECK_INTERVAL = 30000; // Check every 30 seconds during shutdown
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`);
// 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);
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('└──────────────────────────────────────────┘');
try {
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"');
} catch (error) {
console.error('Emergency shutdown failed, trying alternative method...');
await execAsync('poweroff --force');
}
// 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);
await this.sleep(CHECK_INTERVAL);
}
}
console.log('UPS monitoring during shutdown completed');
}
/**
* Sleep for the specified milliseconds

View File

@ -1,13 +1,9 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import * as dgram from 'dgram';
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js';
import { UpsOidSets } from './oid-sets.js';
import { SnmpPacketCreator } from './packet-creator.js';
import { SnmpPacketParser } from './packet-parser.js';
const execAsync = promisify(exec);
/**
* Class for SNMP communication with UPS devices
* Main entry point for SNMP functionality
@ -352,6 +348,14 @@ export class NupstSnmp {
} else if (powerStatusValue === 3) {
powerStatus = 'onBattery';
}
} else if (config.upsModel === 'eaton') {
// Eaton UPS: xupsOutputSource values
// 3=normal/mains, 5=battery, etc.
if (powerStatusValue === 3) {
powerStatus = 'online';
} else if (powerStatusValue === 5) {
powerStatus = 'onBattery';
}
} else {
// Default interpretation for other UPS models
if (powerStatusValue === 1) {
@ -361,14 +365,21 @@ export class NupstSnmp {
}
}
// Convert TimeTicks to minutes for CyberPower runtime (value is in 1/100 seconds)
// Convert to minutes for UPS models with different time units
let processedRuntime = batteryRuntime;
if (config.upsModel === 'cyberpower' && batteryRuntime > 0) {
// TimeTicks is in 1/100 seconds, convert to minutes
// CyberPower: TimeTicks is in 1/100 seconds, convert to minutes
processedRuntime = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
if (this.debug) {
console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${processedRuntime} minutes`);
}
} else if (config.upsModel === 'eaton' && batteryRuntime > 0) {
// Eaton: Runtime is in seconds, convert to minutes
processedRuntime = Math.floor(batteryRuntime / 60);
if (this.debug) {
console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${processedRuntime} minutes`);
}
}
const result = {
@ -507,26 +518,5 @@ export class NupstSnmp {
});
}
/**
* Initiate system shutdown
* @param reason Reason for shutdown
*/
public async initiateShutdown(reason: string): Promise<void> {
console.log(`Initiating system shutdown due to: ${reason}`);
try {
// Execute shutdown command with 5 minute delay to allow for VM graceful shutdown
const { stdout } = await execAsync('shutdown -h +5 "UPS battery critical, shutting down in 5 minutes"');
console.log('Shutdown initiated:', stdout);
console.log('Allowing 5 minutes for VMs to shut down safely');
} catch (error) {
console.error('Failed to initiate shutdown:', error);
// Try a different method if first one fails
try {
console.log('Trying alternative shutdown method...');
await execAsync('poweroff --force');
} catch (innerError) {
console.error('All shutdown methods failed:', innerError);
}
}
}
// initiateShutdown method has been moved to the NupstDaemon class
}

View File

@ -25,9 +25,9 @@ export class UpsOidSets {
// Eaton OIDs
eaton: {
POWER_STATUS: '1.3.6.1.4.1.534.1.1.2.0', // Power status
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // Remaining runtime in minutes
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery)
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
},
// TrippLite OIDs