initial
This commit is contained in:
commit
1e74fbe367
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Build
|
||||
dist*/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Bundled Node.js binaries
|
||||
vendor/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
|
||||
# OS specific
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.nogit/
|
49
bin/nupst
Executable file
49
bin/nupst
Executable file
@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NUPST Launcher Script
|
||||
# This script detects architecture and OS, then runs NUPST with the appropriate Node.js binary
|
||||
|
||||
# First, handle symlinks correctly
|
||||
REAL_SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")
|
||||
SCRIPT_DIR=$(dirname "$REAL_SCRIPT_PATH")
|
||||
|
||||
# For debugging
|
||||
# echo "Script path: $REAL_SCRIPT_PATH"
|
||||
# echo "Script dir: $SCRIPT_DIR"
|
||||
|
||||
# If we're run via symlink from /usr/local/bin, use the hardcoded installation path
|
||||
if [[ "$SCRIPT_DIR" == "/usr/local/bin" ]]; then
|
||||
PROJECT_ROOT="/opt/nupst"
|
||||
else
|
||||
# Otherwise, use relative path from script location
|
||||
PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )"
|
||||
fi
|
||||
|
||||
# For debugging
|
||||
# echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
# Set Node.js binary path directly
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-linux-x64/bin/node"
|
||||
|
||||
# If binary doesn't exist, try system Node as fallback
|
||||
if [ ! -f "$NODE_BIN" ]; then
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_BIN="node"
|
||||
echo "Using system Node.js installation"
|
||||
else
|
||||
echo "Error: Node.js binary not found at $NODE_BIN"
|
||||
echo "Please run the setup script or install Node.js manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run NUPST with the Node.js binary
|
||||
if [ -f "$PROJECT_ROOT/dist_ts/index.js" ]; then
|
||||
exec "$NODE_BIN" "$PROJECT_ROOT/dist_ts/index.js" "$@"
|
||||
elif [ -f "$PROJECT_ROOT/dist/index.js" ]; then
|
||||
exec "$NODE_BIN" "$PROJECT_ROOT/dist/index.js" "$@"
|
||||
else
|
||||
echo "Error: Could not find NUPST's index.js file."
|
||||
echo "Please run the setup script to download the required files."
|
||||
exit 1
|
||||
fi
|
184
changelog.md
Normal file
184
changelog.md
Normal file
@ -0,0 +1,184 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-03-25 - 1.7.6 - fix(core)
|
||||
Refactor SNMP, systemd, and CLI modules to improve error handling, logging, and code clarity
|
||||
|
||||
- Removed unused dependency 'net-snmp' from package.json
|
||||
- Extracted helper functions for SNMP packet creation and parsing (using SnmpEncoder, SnmpPacketCreator and SnmpPacketParser)
|
||||
- Improved debug logging and added detailed documentation comments across SNMP, systemd, CLI and daemon modules
|
||||
- Refactored systemd service management to extract status display and service disabling logic
|
||||
- Updated test suite to use proper modular methods from the new SNMP utilities
|
||||
|
||||
## 2025-03-25 - 1.7.5 - fix(cli)
|
||||
Enable SNMP debug mode in CLI commands and update debug flag handling in daemon-start and test; bump version to 1.7.4
|
||||
|
||||
- Call enableDebug() on SNMP client earlier in command parsing
|
||||
- Pass debug flag to 'daemon-start' and 'test' commands for consistent debug output
|
||||
- Update package version from 1.7.3 to 1.7.4
|
||||
|
||||
## 2025-03-25 - 1.7.3 - fix(SNMP)
|
||||
Refine SNMP packet creation and response parsing for more reliable UPS status monitoring
|
||||
|
||||
- Improve error handling and fallback logic when parsing SNMP responses
|
||||
- Optimize TimeTicks conversion for CyberPower UPS devices
|
||||
- Enhance test coverage for various UPS scenarios
|
||||
|
||||
## 2025-03-25 - 1.7.2 - fix(core)
|
||||
Refactor internal SNMP response parsing and enhance daemon logging for improved error reporting and clarity.
|
||||
|
||||
- Improved fallback and error handling in SNMP response parsing
|
||||
- Enhanced logging messages in daemon and systemd service management
|
||||
- Minor refactoring for better maintainability without functional changes
|
||||
|
||||
## 2025-03-25 - 1.7.1 - fix(snmp-cli)
|
||||
Improve SNMP response parsing and CLI UPS connection timeout handling
|
||||
|
||||
- Expand parsing loop in SNMP responses to capture Gauge32 and Timeticks values
|
||||
- Add detailed debug logging for both SNMP v1/v2 and v3 responses
|
||||
- Configure CLI test commands to use a shortened timeout for UPS connection tests
|
||||
|
||||
## 2025-03-25 - 1.7.0 - feat(SNMP/UPS)
|
||||
Add UPS model selection and custom OIDs support to handle different UPS brands
|
||||
|
||||
- Introduce distinct OID sets for CyberPower, APC, Eaton, TrippLite, Liebert, and a custom option
|
||||
- Update interactive setup to prompt for UPS model selection and custom OID entry when needed
|
||||
- Refactor SNMP status retrieval to dynamically select the appropriate OIDs based on the configured UPS model
|
||||
- Extend default configuration with an upsModel property for consistent behavior
|
||||
|
||||
## 2025-03-25 - 1.6.0 - feat(cli,snmp)
|
||||
Enhance debug logging and add debug mode support in CLI and SNMP modules
|
||||
|
||||
- Enable debug flags (--debug, -d) in CLI to trigger detailed SNMP logging
|
||||
- Pass debug mode options to daemonStart and test commands for improved diagnostics
|
||||
- Add enableDebug method to the SNMP module for on-demand debug logging
|
||||
- Improve timeout and discovery logging details for streamlined troubleshooting
|
||||
|
||||
## 2025-03-25 - 1.5.0 - feat(cli)
|
||||
Enhance CLI output: display SNMPv3 auth/priv details and support timeout customization during setup
|
||||
|
||||
- Display authentication and privacy protocol details when SNMP version is 3
|
||||
- Show timeout value in the configuration test output
|
||||
- Clear out unused authentication or privacy settings based on selected security level
|
||||
- Allow users to customize SNMP timeout during interactive setup
|
||||
|
||||
## 2025-03-25 - 1.4.1 - fix(version)
|
||||
Bump patch version for consistency with commit info
|
||||
|
||||
|
||||
## 2025-03-25 - 1.4.0 - feat(snmp)
|
||||
Implement native SNMPv3 support with simulated encryption and enhanced authentication handling.
|
||||
|
||||
- Add fully native SNMPv3 GET request implementation replacing the snmpwalk fallback
|
||||
- Simulate encryption for authPriv using AES and DES protocols
|
||||
- Enhance SNMP response parser to properly handle SNMPv3 responses
|
||||
- Introduce detailed security parameter management for SNMPv3
|
||||
|
||||
## 2025-03-25 - 1.3.1 - fix(cli)
|
||||
Remove redundant SNMP tools checks in CLI and Systemd modules
|
||||
|
||||
- Eliminate unnecessary snmpwalk dependency checks in the test command and interactive setup flow.
|
||||
- Adjust systemd configuration file check to avoid external dependency verification.
|
||||
|
||||
## 2025-03-25 - 1.3.0 - feat(cli)
|
||||
add test command to verify UPS SNMP configuration and connectivity
|
||||
|
||||
- Introduce a new 'test' command in the CLI to check the SNMP configuration and UPS connection.
|
||||
- Validate installation of SNMP tools and configuration file before testing.
|
||||
- Output UPS status details and compare against defined shutdown thresholds.
|
||||
|
||||
## 2025-03-25 - 1.2.6 - fix(cli)
|
||||
Refactor interactive setup to use dynamic import for readline and ensure proper cleanup
|
||||
|
||||
- Replaced synchronous require() with async import for ESM compatibility
|
||||
- Wrapped interactive setup in try/finally to guarantee readline interface closure
|
||||
- Enhanced error logging by outputting error.message
|
||||
|
||||
## 2025-03-25 - 1.2.5 - fix(error-handling)
|
||||
Improve error handling in CLI, daemon, and systemd lifecycle management with enhanced logging for configuration issues
|
||||
|
||||
- Wrap daemon and service start commands in try-catch blocks to properly handle and log errors
|
||||
- Throw explicit errors when configuration file is missing instead of silently defaulting
|
||||
- Enhance log messages for service installation, startup, and status retrieval for clearer debugging
|
||||
|
||||
## 2025-03-25 - 1.2.4 - fix(cli/daemon)
|
||||
Improve logging and user feedback in interactive setup and UPS monitoring
|
||||
|
||||
- Refactor configuration summary output in the interactive setup for clearer display
|
||||
- Enhance logging in the daemon to show detailed SNMP settings and UPS status changes
|
||||
- Improve error messages and user guidance during configuration and monitoring
|
||||
|
||||
## 2025-03-24 - 1.2.3 - fix(nupst)
|
||||
No changes
|
||||
|
||||
|
||||
## 2025-03-24 - 1.2.2 - fix(bin/nupst)
|
||||
Improve symlink resolution in launcher script to correctly determine project root based on execution path.
|
||||
|
||||
- Replace directory determination with readlink for accurate symlink resolution
|
||||
- Set project root to '/opt/nupst' when script is run via symlink from /usr/local/bin
|
||||
- Add debugging comments to assist with path resolution
|
||||
|
||||
## 2025-03-24 - 1.2.1 - fix(bin)
|
||||
Simplify Node.js binary detection in installation script
|
||||
|
||||
- Directly set Node binary path to vendor/node-linux-x64/bin/node
|
||||
- Remove redundant architecture-specific detection logic
|
||||
- Fallback to system Node if vendor binary is not found
|
||||
|
||||
## 2025-03-24 - 1.2.0 - feat(installer)
|
||||
Improve Node.js binary detection and dynamic LTS version retrieval in setup scripts
|
||||
|
||||
- Enhanced bin/nupst to search multiple possible locations for the Node.js binary and fallback to system node if necessary
|
||||
- Updated setup.sh to fetch the latest LTS Node.js version from nodejs.org and use a fallback version when the request fails
|
||||
|
||||
## 2025-03-24 - 1.1.2 - fix(setup.sh)
|
||||
Improve error handling in setup.sh: exit immediately when the downloaded npm package lacks the dist_ts directory, removing the fallback build-from-source mechanism.
|
||||
|
||||
- Removed BUILD_FROM_SOURCE logic that attempted to build from source on missing dist_ts directory
|
||||
- Updated error messages to clearly indicate failure in downloading a valid package
|
||||
- Ensured installation halts if essential files are missing
|
||||
|
||||
## 2025-03-24 - 1.1.1 - fix(package.json)
|
||||
Remove unused prepublishOnly script and update files field in package.json
|
||||
|
||||
- Removed prepublishOnly build trigger
|
||||
- Updated files list to accurately include intended directories and files
|
||||
|
||||
## 2025-03-24 - 1.1.0 - feat(installer-setup)
|
||||
Enhance installer and setup scripts for improved global installation and artifact management
|
||||
|
||||
- Detect piped installation in install.sh, clone repository automatically, and clean up previous installations
|
||||
- Update readme.md with correct repository URL and clearer installation instructions
|
||||
- Improve setup.sh to remove existing dist_ts, download build artifacts from the npm registry, and simplify dependency installation
|
||||
|
||||
## 2025-03-24 - 1.0.1 - fix(version)
|
||||
Bump version to 1.0.1
|
||||
|
||||
- Updated commitinfo data to reflect the new patch version.
|
||||
- Synchronized version information between commitinfo file and package metadata.
|
||||
|
||||
## 2025-03-24 - 1.0.1 - fix(build)
|
||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and adjust distribution paths in .gitignore
|
||||
|
||||
- Replaced 'tsc' with 'tsbuild tsfolders --allowimplicitany' in package.json
|
||||
- Updated .gitignore to reflect new compiled distribution folder pattern
|
||||
- Updated changelog to document build improvements and regenerated type definitions
|
||||
|
||||
## 2025-03-24 - 1.0.1 - fix(build)
|
||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type definitions for CLI, daemon, index, nupst, snmp, and systemd modules
|
||||
|
||||
- Replaced 'tsc' command with tsbuild in package.json
|
||||
- Updated .gitignore to reflect new compiled distribution folder pattern
|
||||
- Added new dist_ts files including .d.ts type definitions and compiled JavaScript for multiple modules
|
||||
|
||||
## 2025-03-24 - 1.0.1 - fix(build)
|
||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type definitions for CLI, daemon, nupst, snmp, and systemd modules.
|
||||
|
||||
- Replaced the 'tsc' command with 'tsbuild tsfolders --allowimplicitany' in package.json.
|
||||
- Added new dist_ts files including type definitions (d.ts) and compiled JavaScript for CLI, daemon, index, nupst, snmp, and systemd.
|
||||
- Improved the generated CLI declarations and overall distribution build.
|
||||
|
||||
## 2025-03-23 - 1.0.0 - initial setup
|
||||
This range covers the early commits that mainly established the repository structure.
|
||||
|
||||
- Initial repository commit with basic project initialization.
|
97
install.sh
Normal file
97
install.sh
Normal file
@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NUPST Installer Script
|
||||
# Downloads and installs NUPST globally on the system
|
||||
# Can be used directly with curl: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root (sudo bash install.sh or pipe to sudo bash)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect if script is being piped or run directly
|
||||
PIPED=0
|
||||
if [ ! -t 0 ]; then
|
||||
# Being piped, need to clone the repo
|
||||
PIPED=1
|
||||
fi
|
||||
|
||||
# Define installation directory
|
||||
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
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "Git is required but not installed. Please install git first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if installation directory exists
|
||||
if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then
|
||||
echo "Existing installation found at $INSTALL_DIR. Updating..."
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# Try to update the repository
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to update repository. Reinstalling..."
|
||||
cd /
|
||||
rm -rf "$INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
|
||||
else
|
||||
echo "Repository updated successfully."
|
||||
fi
|
||||
else
|
||||
# Fresh installation
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Removing previous installation at $INSTALL_DIR..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# Create installation directory
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Clone the repository
|
||||
echo "Cloning NUPST repository to $INSTALL_DIR..."
|
||||
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to clone/update repository. Please check your internet connection."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set script directory to the cloned repo
|
||||
SCRIPT_DIR="$INSTALL_DIR"
|
||||
else
|
||||
# Running directly from within the repo
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
fi
|
||||
|
||||
# Run setup script
|
||||
echo "Running setup script..."
|
||||
bash "$SCRIPT_DIR/setup.sh"
|
||||
|
||||
# Install globally
|
||||
echo "Installing NUPST globally..."
|
||||
ln -sf "$SCRIPT_DIR/bin/nupst" /usr/local/bin/nupst
|
||||
|
||||
# Installation completed
|
||||
if [ $PIPED -eq 1 ]; then
|
||||
echo "NUPST has been installed globally at $INSTALL_DIR"
|
||||
else
|
||||
echo "NUPST has been installed globally."
|
||||
fi
|
||||
|
||||
echo "You can now run 'nupst' from anywhere."
|
||||
echo ""
|
||||
echo "To get started, try:"
|
||||
echo " nupst help"
|
||||
echo " nupst setup # To configure your UPS connection"
|
1
npmextra.json
Normal file
1
npmextra.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
58
package.json
Normal file
58
package.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "1.7.6",
|
||||
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"nupst": "bin/nupst"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsbuild tsfolders --allowimplicitany",
|
||||
"start": "bin/nupst",
|
||||
"setup": "bash setup.sh",
|
||||
"test": "tstest test/",
|
||||
"install-global": "sudo bash install.sh",
|
||||
"uninstall": "sudo bash uninstall.sh"
|
||||
},
|
||||
"keywords": [
|
||||
"ups",
|
||||
"snmp",
|
||||
"shutdown",
|
||||
"node",
|
||||
"cli"
|
||||
],
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"ts_web/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.3.2",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.96",
|
||||
"@push.rocks/qenv": "^6.1.0",
|
||||
"@push.rocks/tapbundle": "^5.6.0",
|
||||
"@types/node": "^20.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"mongodb-memory-server",
|
||||
"puppeteer"
|
||||
]
|
||||
}
|
||||
}
|
10187
pnpm-lock.yaml
generated
Normal file
10187
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
0
readme.hints.md
Normal file
0
readme.hints.md
Normal file
146
readme.md
Normal file
146
readme.md
Normal file
@ -0,0 +1,146 @@
|
||||
# NUPST - Node.js UPS Shutdown Tool
|
||||
|
||||
NUPST is a command-line tool that monitors SNMP-enabled UPS devices and initiates system shutdown when power outages are detected and battery levels are low.
|
||||
|
||||
## Features
|
||||
|
||||
- Monitors UPS devices using SNMP
|
||||
- Automatic shutdown when battery level falls below threshold
|
||||
- Automatic shutdown when runtime remaining falls below threshold
|
||||
- Simple systemd service integration
|
||||
- Self-contained - includes its own Node.js runtime
|
||||
|
||||
## Installation
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### Direct from Git
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://code.foss.global/serve.zone/nupst.git
|
||||
cd nupst
|
||||
|
||||
# Option 1: Quick install (requires root privileges)
|
||||
sudo ./install.sh
|
||||
|
||||
# Option 2: Manual setup
|
||||
./setup.sh
|
||||
sudo ln -s $(pwd)/bin/nupst /usr/local/bin/nupst
|
||||
```
|
||||
|
||||
### From NPM
|
||||
|
||||
```bash
|
||||
npm install -g @serve.zone/nupst
|
||||
```
|
||||
|
||||
## Uninstallation
|
||||
|
||||
```bash
|
||||
# If installed from git repository:
|
||||
cd /path/to/nupst
|
||||
sudo ./uninstall.sh
|
||||
|
||||
# If installed from npm:
|
||||
npm uninstall -g @serve.zone/nupst
|
||||
```
|
||||
|
||||
The uninstaller will:
|
||||
- Stop and disable the systemd service (if installed)
|
||||
- Remove the systemd service file
|
||||
- Remove the symlink from /usr/local/bin
|
||||
- Optionally remove configuration files from /etc/nupst
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
NUPST - Node.js UPS Shutdown Tool
|
||||
|
||||
Usage:
|
||||
nupst enable - Install and enable the systemd service (requires root)
|
||||
nupst disable - Stop and uninstall the systemd service (requires root)
|
||||
nupst daemon-start - Start the daemon process directly
|
||||
nupst logs - Show logs of the systemd service
|
||||
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 help - Show this help message
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
NUPST provides an interactive setup to configure your UPS:
|
||||
|
||||
```bash
|
||||
nupst setup
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Alternatively, you can manually edit the configuration file at `/etc/nupst/config.json`. A default configuration will be created on first run:
|
||||
|
||||
```json
|
||||
{
|
||||
"snmp": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 161,
|
||||
"community": "public",
|
||||
"version": 1,
|
||||
"timeout": 5000
|
||||
},
|
||||
"thresholds": {
|
||||
"battery": 60,
|
||||
"runtime": 20
|
||||
},
|
||||
"checkInterval": 30000
|
||||
}
|
||||
```
|
||||
|
||||
- `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)
|
||||
- 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
|
||||
- `thresholds`: When to trigger shutdown
|
||||
- `battery`: Battery percentage threshold (default: 60%)
|
||||
- `runtime`: Runtime minutes threshold (default: 20 minutes)
|
||||
- `checkInterval`: How often to check UPS status in milliseconds (default: 30000)
|
||||
|
||||
## Setup as a Service
|
||||
|
||||
To set up NUPST as a systemd service:
|
||||
|
||||
```bash
|
||||
sudo nupst enable
|
||||
sudo nupst start
|
||||
```
|
||||
|
||||
To check the status:
|
||||
|
||||
```bash
|
||||
nupst status
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
227
setup.sh
Normal file
227
setup.sh
Normal file
@ -0,0 +1,227 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NUPST Setup Script
|
||||
# Downloads the appropriate Node.js binary for the current platform
|
||||
|
||||
# Find the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
# Create vendor directory if it doesn't exist
|
||||
mkdir -p "$SCRIPT_DIR/vendor"
|
||||
|
||||
# Get the latest LTS Node.js version
|
||||
echo "Determining latest LTS Node.js version..."
|
||||
NODE_VERSIONS_JSON=$(curl -s https://nodejs.org/dist/index.json)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Warning: Could not fetch latest Node.js versions. Using fallback version."
|
||||
NODE_VERSION="20.11.1" # Fallback to a recent LTS version
|
||||
else
|
||||
# Extract the latest LTS version (those marked with lts field)
|
||||
NODE_VERSION=$(echo "$NODE_VERSIONS_JSON" | grep -o '"version":"v[0-9.]*".*"lts":[^,]*' | grep -v '"lts":false' | grep -o 'v[0-9.]*' | head -1 | cut -c 2-)
|
||||
|
||||
if [ -z "$NODE_VERSION" ]; then
|
||||
echo "Warning: Could not determine latest LTS version. Using fallback version."
|
||||
NODE_VERSION="20.11.1" # Fallback to a recent LTS version
|
||||
else
|
||||
echo "Latest Node.js LTS version: $NODE_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
OS=$(uname -s)
|
||||
|
||||
# Map architecture and OS to Node.js download URL
|
||||
NODE_URL=""
|
||||
NODE_DIR=""
|
||||
case "$OS" in
|
||||
Linux)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz"
|
||||
NODE_DIR="node-linux-x64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-arm64.tar.gz"
|
||||
NODE_DIR="node-linux-arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: $ARCH. Please install Node.js manually."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
Darwin)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-x64.tar.gz"
|
||||
NODE_DIR="node-darwin-x64"
|
||||
;;
|
||||
arm64)
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-arm64.tar.gz"
|
||||
NODE_DIR="node-darwin-arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: $ARCH. Please install Node.js manually."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported operating system: $OS. Please install Node.js manually."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check if we already have the Node.js binary
|
||||
if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ]; then
|
||||
echo "Node.js binary already exists for $OS-$ARCH. Skipping download."
|
||||
else
|
||||
echo "Downloading Node.js v$NODE_VERSION for $OS-$ARCH..."
|
||||
|
||||
# Download and extract Node.js
|
||||
TMP_FILE="$SCRIPT_DIR/vendor/node.tar.gz"
|
||||
curl -L "$NODE_URL" -o "$TMP_FILE"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error downloading Node.js. Please check your internet connection and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create target directory
|
||||
mkdir -p "$SCRIPT_DIR/vendor/$NODE_DIR"
|
||||
|
||||
# Extract Node.js
|
||||
tar -xzf "$TMP_FILE" -C "$SCRIPT_DIR/vendor"
|
||||
|
||||
# Move extracted files to the target directory
|
||||
NODE_EXTRACT_DIR=$(find "$SCRIPT_DIR/vendor" -maxdepth 1 -name "node-v*" -type d | head -n 1)
|
||||
if [ -d "$NODE_EXTRACT_DIR" ]; then
|
||||
cp -R "$NODE_EXTRACT_DIR"/* "$SCRIPT_DIR/vendor/$NODE_DIR/"
|
||||
rm -rf "$NODE_EXTRACT_DIR"
|
||||
else
|
||||
echo "Error extracting Node.js. Please try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm "$TMP_FILE"
|
||||
|
||||
echo "Node.js v$NODE_VERSION for $OS-$ARCH has been downloaded and extracted."
|
||||
fi
|
||||
|
||||
# Remove any existing dist_ts directory
|
||||
if [ -d "$SCRIPT_DIR/dist_ts" ]; then
|
||||
echo "Removing existing dist_ts directory..."
|
||||
rm -rf "$SCRIPT_DIR/dist_ts"
|
||||
fi
|
||||
|
||||
# Download dist_ts from npm registry
|
||||
echo "Downloading dist_ts from npm registry..."
|
||||
|
||||
# Create temp directory
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
|
||||
# Get version from package.json
|
||||
if [ -f "$SCRIPT_DIR/package.json" ]; then
|
||||
echo "Reading version from package.json..."
|
||||
# Extract version using grep and cut
|
||||
VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json" | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Could not determine version from package.json."
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Package version is $VERSION. Downloading matching package tarball..."
|
||||
else
|
||||
echo "Warning: package.json not found. Getting latest version from npm registry..."
|
||||
VERSION=$(curl -s https://registry.npmjs.org/@serve.zone/nupst | grep -o '"latest":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Could not determine version from npm registry."
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Latest version is $VERSION. Using as fallback."
|
||||
fi
|
||||
|
||||
# First try to download with the version from package.json
|
||||
TARBALL_URL="https://registry.npmjs.org/@serve.zone/nupst/-/nupst-$VERSION.tgz"
|
||||
TARBALL_PATH="$TEMP_DIR/nupst.tgz"
|
||||
|
||||
echo "Attempting to download version $VERSION from $TARBALL_URL..."
|
||||
curl -sL "$TARBALL_URL" -o "$TARBALL_PATH"
|
||||
|
||||
# If download fails or file is empty, try to get the latest version from npm
|
||||
if [ $? -ne 0 ] || [ ! -s "$TARBALL_PATH" ]; then
|
||||
echo "Package version $VERSION not found on npm registry."
|
||||
echo "Fetching latest version information from npm registry..."
|
||||
|
||||
# Get latest version from npm registry
|
||||
NPM_REGISTRY_INFO=$(curl -s https://registry.npmjs.org/@serve.zone/nupst)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Could not connect to npm registry."
|
||||
echo "Will attempt to build from source instead."
|
||||
rm -rf "$TEMP_DIR"
|
||||
mkdir -p "$SCRIPT_DIR/dist_ts"
|
||||
BUILD_FROM_SOURCE=1
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Extract latest version
|
||||
LATEST_VERSION=$(echo "$NPM_REGISTRY_INFO" | grep -o '"latest":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
echo "Error: Could not determine latest version from npm registry."
|
||||
echo "Will attempt to build from source instead."
|
||||
rm -rf "$TEMP_DIR"
|
||||
mkdir -p "$SCRIPT_DIR/dist_ts"
|
||||
BUILD_FROM_SOURCE=1
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Found latest version: $LATEST_VERSION. Downloading..."
|
||||
|
||||
TARBALL_URL="https://registry.npmjs.org/@serve.zone/nupst/-/nupst-$LATEST_VERSION.tgz"
|
||||
TARBALL_PATH="$TEMP_DIR/nupst.tgz"
|
||||
|
||||
curl -sL "$TARBALL_URL" -o "$TARBALL_PATH"
|
||||
|
||||
if [ $? -ne 0 ] || [ ! -s "$TARBALL_PATH" ]; then
|
||||
echo "Error: Failed to download any package version from npm registry."
|
||||
echo "Installation cannot continue without the dist_ts directory."
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract the tarball
|
||||
mkdir -p "$TEMP_DIR/extract"
|
||||
tar -xzf "$TARBALL_PATH" -C "$TEMP_DIR/extract"
|
||||
|
||||
# Copy dist_ts to the installation directory
|
||||
if [ -d "$TEMP_DIR/extract/package/dist_ts" ]; then
|
||||
echo "Copying dist_ts directory to installation..."
|
||||
mkdir -p "$SCRIPT_DIR/dist_ts"
|
||||
cp -R "$TEMP_DIR/extract/package/dist_ts/"* "$SCRIPT_DIR/dist_ts/"
|
||||
else
|
||||
echo "Error: dist_ts directory not found in the downloaded npm package."
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo "dist_ts directory successfully downloaded from npm registry."
|
||||
|
||||
# Make launcher script executable
|
||||
chmod +x "$SCRIPT_DIR/bin/nupst"
|
||||
|
||||
echo "NUPST setup completed successfully."
|
||||
echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst"
|
||||
echo "To install NUPST globally, run: sudo ln -s $SCRIPT_DIR/bin/nupst /usr/local/bin/nupst"
|
336
test/test.ts
Normal file
336
test/test.ts
Normal file
@ -0,0 +1,336 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { NupstSnmp } from '../ts/snmp.js';
|
||||
import type { SnmpConfig, UpsStatus } from '../ts/snmp.js';
|
||||
import { SnmpEncoder } from '../ts/snmp/encoder.js';
|
||||
import { SnmpPacketCreator } from '../ts/snmp/packet-creator.js';
|
||||
import { SnmpPacketParser } from '../ts/snmp/packet-parser.js';
|
||||
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
const testQenv = new qenv.Qenv('./', '.nogit/');
|
||||
|
||||
// Create an SNMP instance with debug enabled
|
||||
const snmp = new NupstSnmp(true);
|
||||
|
||||
// Load the test configuration from .nogit/env.json
|
||||
const testConfig = await testQenv.getEnvVarOnDemandAsObject('testConfig');
|
||||
|
||||
tap.test('should log config', async () => {
|
||||
console.log(testConfig);
|
||||
});
|
||||
|
||||
tap.test('SNMP packet creation and parsing test', async () => {
|
||||
// We'll test the internal methods that are now in separate classes
|
||||
|
||||
// Test OID conversion
|
||||
const oidStr = '1.3.6.1.4.1.3808.1.1.1.4.1.1.0';
|
||||
const oidArray = SnmpEncoder.oidToArray(oidStr);
|
||||
console.log('OID array length:', oidArray.length);
|
||||
console.log('OID array:', oidArray);
|
||||
// The OID has 14 elements after splitting
|
||||
expect(oidArray.length).toEqual(14);
|
||||
expect(oidArray[0]).toEqual(1);
|
||||
expect(oidArray[1]).toEqual(3);
|
||||
|
||||
// Test OID encoding
|
||||
const encodedOid = SnmpEncoder.encodeOID(oidArray);
|
||||
expect(encodedOid).toBeInstanceOf(Buffer);
|
||||
|
||||
// Test SNMP request creation
|
||||
const request = SnmpPacketCreator.createSnmpGetRequest(oidStr, 'public', true);
|
||||
expect(request).toBeInstanceOf(Buffer);
|
||||
expect(request.length).toBeGreaterThan(20);
|
||||
|
||||
// Log the request for debugging
|
||||
console.log('SNMP Request buffer:', request.toString('hex'));
|
||||
|
||||
// Test integer encoding
|
||||
const int = SnmpEncoder.encodeInteger(42);
|
||||
expect(int).toBeInstanceOf(Buffer);
|
||||
expect(int.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Test SNMPv3 engine ID discovery message
|
||||
const discoveryMsg = SnmpPacketCreator.createDiscoveryMessage(testConfig, 1);
|
||||
expect(discoveryMsg).toBeInstanceOf(Buffer);
|
||||
expect(discoveryMsg.length).toBeGreaterThan(20);
|
||||
|
||||
console.log('SNMPv3 Discovery message:', discoveryMsg.toString('hex'));
|
||||
});
|
||||
|
||||
tap.test('SNMP response parsing simulation', async () => {
|
||||
// Create a simulated SNMP response for parsing
|
||||
|
||||
// Simulate an INTEGER response (battery capacity)
|
||||
const intResponse = Buffer.from([
|
||||
0x30, 0x29, // Sequence, length 41
|
||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
||||
0xa2, 0x1c, // GetResponse
|
||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
||||
0x30, 0x11, // Sequence (varbinds)
|
||||
0x30, 0x0f, // Sequence (varbind)
|
||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example)
|
||||
0x02, 0x01, 0x64 // Integer (value), value 100 (100%)
|
||||
]);
|
||||
|
||||
// Simulate a Gauge32 response (battery capacity)
|
||||
const gauge32Response = Buffer.from([
|
||||
0x30, 0x29, // Sequence, length 41
|
||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
||||
0xa2, 0x1c, // GetResponse
|
||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
||||
0x30, 0x11, // Sequence (varbinds)
|
||||
0x30, 0x0f, // Sequence (varbind)
|
||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example)
|
||||
0x42, 0x01, 0x64 // Gauge32 (value), value 100 (100%)
|
||||
]);
|
||||
|
||||
// Simulate a TimeTicks response (battery runtime)
|
||||
const timeTicksResponse = Buffer.from([
|
||||
0x30, 0x29, // Sequence, length 41
|
||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
||||
0xa2, 0x1c, // GetResponse
|
||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
||||
0x30, 0x11, // Sequence (varbinds)
|
||||
0x30, 0x0f, // Sequence (varbind)
|
||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example)
|
||||
0x43, 0x01, 0x0f // TimeTicks (value), value 15 (0.15 seconds or 15/100 seconds)
|
||||
]);
|
||||
|
||||
// Test parsing INTEGER response
|
||||
const intValue = SnmpPacketParser.parseSnmpResponse(intResponse, testConfig, true);
|
||||
console.log('Parsed INTEGER value:', intValue);
|
||||
expect(intValue).toEqual(100);
|
||||
|
||||
// Test parsing Gauge32 response
|
||||
const gauge32Value = SnmpPacketParser.parseSnmpResponse(gauge32Response, testConfig, true);
|
||||
console.log('Parsed Gauge32 value:', gauge32Value);
|
||||
expect(gauge32Value).toEqual(100);
|
||||
|
||||
// Test parsing TimeTicks response
|
||||
const timeTicksValue = SnmpPacketParser.parseSnmpResponse(timeTicksResponse, testConfig, true);
|
||||
console.log('Parsed TimeTicks value:', timeTicksValue);
|
||||
expect(timeTicksValue).toEqual(15);
|
||||
});
|
||||
|
||||
tap.test('CyberPower TimeTicks conversion', async () => {
|
||||
// Test the conversion of TimeTicks to minutes for CyberPower UPS
|
||||
|
||||
// Set up a config for CyberPower
|
||||
const cyberPowerConfig: SnmpConfig = {
|
||||
...testConfig,
|
||||
upsModel: 'cyberpower'
|
||||
};
|
||||
|
||||
// Create a simulated TimeTicks response with a value of 104 (104/100 seconds)
|
||||
const ticksResponse = Buffer.from([
|
||||
0x30, 0x29, // Sequence
|
||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
||||
0xa2, 0x1c, // GetResponse
|
||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
||||
0x30, 0x11, // Sequence (varbinds)
|
||||
0x30, 0x0f, // Sequence (varbind)
|
||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime)
|
||||
0x43, 0x01, 0x68 // TimeTicks (value), value 104 (104/100 seconds)
|
||||
]);
|
||||
|
||||
// Mock the getUpsStatus function to test our TimeTicks conversion logic
|
||||
const mockGetUpsStatus = async () => {
|
||||
// Parse the TimeTicks value from the response
|
||||
const runtime = SnmpPacketParser.parseSnmpResponse(ticksResponse, testConfig, true);
|
||||
console.log('Raw runtime value:', runtime);
|
||||
|
||||
// Create a sample UPS status result
|
||||
const result = {
|
||||
powerStatus: 'onBattery',
|
||||
batteryCapacity: 100,
|
||||
batteryRuntime: 0,
|
||||
raw: {
|
||||
powerStatus: 2,
|
||||
batteryCapacity: 100,
|
||||
batteryRuntime: runtime,
|
||||
},
|
||||
};
|
||||
|
||||
// Convert TimeTicks to minutes for CyberPower
|
||||
if (cyberPowerConfig.upsModel === 'cyberpower' && runtime > 0) {
|
||||
result.batteryRuntime = Math.floor(runtime / 6000);
|
||||
console.log(`Converting CyberPower runtime from ${runtime} ticks to ${result.batteryRuntime} minutes`);
|
||||
} else {
|
||||
result.batteryRuntime = runtime;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Call our mock function
|
||||
const status = await mockGetUpsStatus();
|
||||
|
||||
// Assert the conversion worked correctly
|
||||
console.log('Final status object:', status);
|
||||
expect(status.batteryRuntime).toEqual(0); // 104 ticks / 6000 = 0.0173... rounds to 0 minutes
|
||||
});
|
||||
|
||||
tap.test('Simulate fully charged online UPS', async () => {
|
||||
// Test a realistic scenario of an online UPS with high battery capacity and ~30 mins runtime
|
||||
|
||||
// Create simulated responses for power status (online), battery capacity (95%), runtime (30 min)
|
||||
|
||||
// Power Status = 2 (online for CyberPower)
|
||||
const powerStatusResponse = Buffer.from([
|
||||
0x30, 0x29, // Sequence
|
||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
||||
0xa2, 0x1c, // GetResponse
|
||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
||||
0x30, 0x11, // Sequence (varbinds)
|
||||
0x30, 0x0f, // Sequence (varbind)
|
||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x01, 0x01, 0x00, // OID (power status)
|
||||
0x02, 0x01, 0x02 // Integer (value), value 2 (online)
|
||||
]);
|
||||
|
||||
// Battery Capacity = 95% (as Gauge32)
|
||||
const batteryCapacityResponse = Buffer.from([
|
||||
0x30, 0x29, // Sequence
|
||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
||||
0xa2, 0x1c, // GetResponse
|
||||
0x02, 0x01, 0x02, // Integer (request ID), value 2
|
||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
||||
0x30, 0x11, // Sequence (varbinds)
|
||||
0x30, 0x0f, // Sequence (varbind)
|
||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x01, 0x00, // OID (battery capacity)
|
||||
0x42, 0x01, 0x5F // Gauge32 (value), value 95 (95%)
|
||||
]);
|
||||
|
||||
// Battery Runtime = 30 minutes (as TimeTicks)
|
||||
// 30 minutes = 1800 seconds = 180000 ticks (in 1/100 seconds)
|
||||
const batteryRuntimeResponse = Buffer.from([
|
||||
0x30, 0x2c, // Sequence
|
||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
||||
0xa2, 0x1f, // GetResponse
|
||||
0x02, 0x01, 0x03, // Integer (request ID), value 3
|
||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
||||
0x30, 0x14, // Sequence (varbinds)
|
||||
0x30, 0x12, // Sequence (varbind)
|
||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime)
|
||||
0x43, 0x04, 0x00, 0x02, 0xBF, 0x20 // TimeTicks (value), value 180000 (1800 seconds = 30 minutes)
|
||||
]);
|
||||
|
||||
// Mock the getUpsStatus function to test with our simulated data
|
||||
const mockGetUpsStatus = async () => {
|
||||
console.log('Simulating UPS status request with synthetic data');
|
||||
|
||||
// Create a config that specifies this is a CyberPower UPS
|
||||
const upsConfig: SnmpConfig = {
|
||||
host: '192.168.1.1',
|
||||
port: 161,
|
||||
version: 1,
|
||||
community: 'public',
|
||||
timeout: 5000,
|
||||
upsModel: 'cyberpower',
|
||||
};
|
||||
|
||||
// Parse each simulated response
|
||||
const powerStatus = SnmpPacketParser.parseSnmpResponse(powerStatusResponse, upsConfig, true);
|
||||
console.log('Power status value:', powerStatus);
|
||||
|
||||
const batteryCapacity = SnmpPacketParser.parseSnmpResponse(batteryCapacityResponse, upsConfig, true);
|
||||
console.log('Battery capacity value:', batteryCapacity);
|
||||
|
||||
const batteryRuntime = SnmpPacketParser.parseSnmpResponse(batteryRuntimeResponse, upsConfig, true);
|
||||
console.log('Battery runtime value:', batteryRuntime);
|
||||
|
||||
// Convert TimeTicks to minutes for CyberPower UPSes
|
||||
const runtimeMinutes = Math.floor(batteryRuntime / 6000);
|
||||
console.log(`Converting ${batteryRuntime} ticks to ${runtimeMinutes} minutes`);
|
||||
|
||||
// Interpret power status for CyberPower
|
||||
// CyberPower: 2=online, 3=on battery
|
||||
let powerStatusText: 'online' | 'onBattery' | 'unknown' = 'unknown';
|
||||
if (powerStatus === 2) {
|
||||
powerStatusText = 'online';
|
||||
} else if (powerStatus === 3) {
|
||||
powerStatusText = 'onBattery';
|
||||
}
|
||||
|
||||
// Create the status result
|
||||
const result: UpsStatus = {
|
||||
powerStatus: powerStatusText,
|
||||
batteryCapacity: batteryCapacity,
|
||||
batteryRuntime: runtimeMinutes,
|
||||
raw: {
|
||||
powerStatus,
|
||||
batteryCapacity,
|
||||
batteryRuntime,
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Call our mock function
|
||||
const status = await mockGetUpsStatus();
|
||||
|
||||
// Assert that the values match our expectations
|
||||
console.log('UPS Status Result:', status);
|
||||
expect(status.powerStatus).toEqual('online');
|
||||
expect(status.batteryCapacity).toEqual(95);
|
||||
expect(status.batteryRuntime).toEqual(30);
|
||||
});
|
||||
|
||||
// Test with real UPS using the configuration from .nogit/env.json
|
||||
tap.test('Real UPS test', async () => {
|
||||
try {
|
||||
console.log('Testing with real UPS configuration...');
|
||||
|
||||
// Extract the correct SNMP config from the test configuration
|
||||
const snmpConfig = testConfig.snmp;
|
||||
console.log('SNMP Config:');
|
||||
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
||||
console.log(` Version: SNMPv${snmpConfig.version}`);
|
||||
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
||||
|
||||
// Use a short timeout for testing
|
||||
const testSnmpConfig = {
|
||||
...snmpConfig,
|
||||
timeout: Math.min(snmpConfig.timeout, 10000) // Use at most 10 seconds for testing
|
||||
};
|
||||
|
||||
// Try to get the UPS status
|
||||
const status = await snmp.getUpsStatus(testSnmpConfig);
|
||||
|
||||
console.log('UPS Status:');
|
||||
console.log(` Power Status: ${status.powerStatus}`);
|
||||
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
||||
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
|
||||
// Just make sure we got valid data types back
|
||||
expect(status).toBeTruthy();
|
||||
expect(['online', 'onBattery', 'unknown']).toContain(status.powerStatus);
|
||||
expect(typeof status.batteryCapacity).toEqual('number');
|
||||
expect(typeof status.batteryRuntime).toEqual('number');
|
||||
} catch (error) {
|
||||
console.log('Real UPS test failed:', error);
|
||||
// Skip the test if we can't connect to the real UPS
|
||||
console.log('Skipping this test since the UPS might not be available');
|
||||
}
|
||||
});
|
||||
|
||||
// Export the default tap object
|
||||
export default tap.start();
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/nupst',
|
||||
version: '1.7.6',
|
||||
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
|
||||
}
|
738
ts/cli.ts
Normal file
738
ts/cli.ts
Normal file
@ -0,0 +1,738 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { Nupst } from './nupst.js';
|
||||
|
||||
/**
|
||||
* Class for handling CLI commands
|
||||
* Provides interface between user commands and the application
|
||||
*/
|
||||
export class NupstCli {
|
||||
private readonly nupst: Nupst;
|
||||
|
||||
/**
|
||||
* Create a new CLI handler
|
||||
*/
|
||||
constructor() {
|
||||
this.nupst = new Nupst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments and execute the appropriate command
|
||||
* @param args Command line arguments (process.argv)
|
||||
*/
|
||||
public async parseAndExecute(args: string[]): Promise<void> {
|
||||
// Extract debug flag from any position
|
||||
const debugOptions = this.extractDebugOptions(args);
|
||||
if (debugOptions.debugMode) {
|
||||
console.log('Debug mode enabled');
|
||||
// Enable debug mode in the SNMP client
|
||||
this.nupst.getSnmp().enableDebug();
|
||||
}
|
||||
|
||||
// Get the command (default to help if none provided)
|
||||
const command = args[2] || 'help';
|
||||
|
||||
// Route to the appropriate command handler
|
||||
await this.executeCommand(command, debugOptions.debugMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and remove debug options from args
|
||||
* @param args Command line arguments
|
||||
* @returns Object with debug flags and cleaned args
|
||||
*/
|
||||
private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } {
|
||||
const debugMode = args.includes('--debug') || args.includes('-d');
|
||||
// Remove debug flags from args
|
||||
const cleanedArgs = args.filter(arg => arg !== '--debug' && arg !== '-d');
|
||||
|
||||
return { debugMode, cleanedArgs };
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the command with the given arguments
|
||||
* @param command Command to execute
|
||||
* @param debugMode Whether debug mode is enabled
|
||||
*/
|
||||
private async executeCommand(command: string, debugMode: boolean): Promise<void> {
|
||||
switch (command) {
|
||||
case 'enable':
|
||||
await this.enable();
|
||||
break;
|
||||
|
||||
case 'daemon-start':
|
||||
await this.daemonStart(debugMode);
|
||||
break;
|
||||
|
||||
case 'logs':
|
||||
await this.logs();
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
await this.stop();
|
||||
break;
|
||||
|
||||
case 'start':
|
||||
await this.start();
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
await this.status();
|
||||
break;
|
||||
|
||||
case 'disable':
|
||||
await this.disable();
|
||||
break;
|
||||
|
||||
case 'setup':
|
||||
await this.setup();
|
||||
break;
|
||||
|
||||
case 'test':
|
||||
await this.test(debugMode);
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
default:
|
||||
this.showHelp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the service (requires root)
|
||||
*/
|
||||
private async enable(): Promise<void> {
|
||||
this.checkRootAccess('This command must be run as root.');
|
||||
await this.nupst.getSystemd().install();
|
||||
console.log('NUPST service has been installed. Use "nupst start" to start the service.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the daemon directly
|
||||
* @param debugMode Whether to enable debug mode
|
||||
*/
|
||||
private async daemonStart(debugMode: boolean = false): Promise<void> {
|
||||
console.log('Starting NUPST daemon...');
|
||||
try {
|
||||
// Enable debug mode for SNMP if requested
|
||||
if (debugMode) {
|
||||
this.nupst.getSnmp().enableDebug();
|
||||
console.log('SNMP debug mode enabled');
|
||||
}
|
||||
await this.nupst.getDaemon().start();
|
||||
} catch (error) {
|
||||
// Error is already logged and process.exit is called in daemon.start()
|
||||
// No need to handle it here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show logs of the systemd service
|
||||
*/
|
||||
private async logs(): Promise<void> {
|
||||
try {
|
||||
const logs = execSync('journalctl -u nupst.service -n 50 -f').toString();
|
||||
console.log(logs);
|
||||
} catch (error) {
|
||||
console.error('Failed to retrieve logs:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the systemd service
|
||||
*/
|
||||
private async stop(): Promise<void> {
|
||||
await this.nupst.getSystemd().stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the systemd service
|
||||
*/
|
||||
private async start(): Promise<void> {
|
||||
try {
|
||||
await this.nupst.getSystemd().start();
|
||||
} catch (error) {
|
||||
// Error will be displayed by systemd.start()
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show status of the systemd service and UPS
|
||||
*/
|
||||
private async status(): Promise<void> {
|
||||
await this.nupst.getSystemd().getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the service (requires root)
|
||||
*/
|
||||
private async disable(): Promise<void> {
|
||||
this.checkRootAccess('This command must be run as root.');
|
||||
await this.nupst.getSystemd().disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has root access
|
||||
* @param errorMessage Error message to display if not root
|
||||
*/
|
||||
private checkRootAccess(errorMessage: string): void {
|
||||
if (process.getuid && process.getuid() !== 0) {
|
||||
console.error(errorMessage);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the current configuration by connecting to the UPS
|
||||
* @param debugMode Whether to enable debug mode
|
||||
*/
|
||||
private async test(debugMode: boolean = false): Promise<void> {
|
||||
try {
|
||||
// Debug mode is now handled in parseAndExecute
|
||||
if (debugMode) {
|
||||
console.log('┌─ Debug Mode ─────────────────────────────┐');
|
||||
console.log('│ SNMP debugging enabled - detailed logs will be shown');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
}
|
||||
|
||||
// Try to load the configuration
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
console.error('┌─ Configuration Error ─────────────────────┐');
|
||||
console.error('│ No configuration found.');
|
||||
console.error('│ Please run \'nupst setup\' first to create a configuration.');
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
this.displayTestConfig(config);
|
||||
await this.testConnection(config);
|
||||
} catch (error) {
|
||||
console.error(`Test failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the configuration for testing
|
||||
* @param config Current configuration
|
||||
*/
|
||||
private displayTestConfig(config: any): void {
|
||||
console.log('┌─ Testing Configuration ─────────────────────┐');
|
||||
console.log('│ SNMP Settings:');
|
||||
console.log(`│ Host: ${config.snmp.host}`);
|
||||
console.log(`│ Port: ${config.snmp.port}`);
|
||||
console.log(`│ Version: ${config.snmp.version}`);
|
||||
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
||||
|
||||
if (config.snmp.version === 1 || config.snmp.version === 2) {
|
||||
console.log(`│ Community: ${config.snmp.community}`);
|
||||
} else if (config.snmp.version === 3) {
|
||||
console.log(`│ Security Level: ${config.snmp.securityLevel}`);
|
||||
console.log(`│ Username: ${config.snmp.username}`);
|
||||
|
||||
// Show auth and privacy details based on security level
|
||||
if (config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv') {
|
||||
console.log(`│ Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
|
||||
}
|
||||
|
||||
if (config.snmp.securityLevel === 'authPriv') {
|
||||
console.log(`│ Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
|
||||
}
|
||||
|
||||
// Show timeout value
|
||||
console.log(`│ Timeout: ${config.snmp.timeout / 1000} seconds`);
|
||||
}
|
||||
|
||||
// Show OIDs if custom model is selected
|
||||
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
|
||||
console.log('│ Custom OIDs:');
|
||||
console.log(`│ Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`);
|
||||
console.log(`│ Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`);
|
||||
console.log(`│ Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
||||
}
|
||||
console.log('│ Thresholds:');
|
||||
console.log(`│ Battery: ${config.thresholds.battery}%`);
|
||||
console.log(`│ Runtime: ${config.thresholds.runtime} minutes`);
|
||||
console.log(`│ Check Interval: ${config.checkInterval / 1000} seconds`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection to the UPS
|
||||
* @param config Current configuration
|
||||
*/
|
||||
private async testConnection(config: any): Promise<void> {
|
||||
console.log('\nTesting connection to UPS...');
|
||||
try {
|
||||
// Create a test config with a short timeout
|
||||
const testConfig = {
|
||||
...config.snmp,
|
||||
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for testing
|
||||
};
|
||||
|
||||
const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
|
||||
|
||||
console.log('┌─ Connection Successful! ─────────────────┐');
|
||||
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('└──────────────────────────────────────────┘');
|
||||
|
||||
// Check status against thresholds if on battery
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
this.analyzeThresholds(status, config);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('┌─ Connection Failed! ───────────────────────┐');
|
||||
console.error(`│ Error: ${error.message}`);
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
console.log('\nPlease check your settings and run \'nupst setup\' to reconfigure.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze UPS status against thresholds
|
||||
* @param status UPS status
|
||||
* @param config Current configuration
|
||||
*/
|
||||
private analyzeThresholds(status: any, config: any): void {
|
||||
console.log('┌─ Threshold Analysis ───────────────────────┐');
|
||||
|
||||
if (status.batteryCapacity < config.thresholds.battery) {
|
||||
console.log('│ ⚠️ WARNING: Battery capacity below threshold');
|
||||
console.log(`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`);
|
||||
console.log('│ System would initiate shutdown');
|
||||
} else {
|
||||
console.log('│ ✓ Battery capacity above threshold');
|
||||
console.log(`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`);
|
||||
}
|
||||
|
||||
if (status.batteryRuntime < config.thresholds.runtime) {
|
||||
console.log('│ ⚠️ WARNING: Runtime below threshold');
|
||||
console.log(`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`);
|
||||
console.log('│ System would initiate shutdown');
|
||||
} else {
|
||||
console.log('│ ✓ Runtime above threshold');
|
||||
console.log(`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`);
|
||||
}
|
||||
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display help message
|
||||
*/
|
||||
private showHelp(): void {
|
||||
console.log(`
|
||||
NUPST - Node.js UPS Shutdown Tool
|
||||
|
||||
Usage:
|
||||
nupst enable - Install and enable the systemd service (requires root)
|
||||
nupst disable - Stop and uninstall the systemd service (requires root)
|
||||
nupst daemon-start - Start the daemon process directly
|
||||
nupst logs - Show logs of the systemd service
|
||||
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
|
||||
nupst help - Show this help message
|
||||
|
||||
Options:
|
||||
--debug, -d - Enable debug mode for detailed SNMP logging
|
||||
(Example: nupst test --debug)
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive setup for configuring SNMP settings
|
||||
*/
|
||||
private async setup(): Promise<void> {
|
||||
try {
|
||||
// Import readline module (ESM style)
|
||||
const readline = await import('readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
// Helper function to prompt for input
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await this.runSetupProcess(prompt);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Setup error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the interactive setup process
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async runSetupProcess(prompt: (question: string) => Promise<string>): Promise<void> {
|
||||
console.log('\nNUPST Interactive Setup');
|
||||
console.log('======================\n');
|
||||
console.log('This will guide you through configuring your UPS SNMP settings.\n');
|
||||
|
||||
// Try to load existing config if available
|
||||
let config;
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
config = this.nupst.getDaemon().getConfig();
|
||||
} catch (error) {
|
||||
// If config doesn't exist, use default config
|
||||
config = this.nupst.getDaemon().getConfig();
|
||||
console.log('No existing configuration found. Creating a new configuration.');
|
||||
}
|
||||
|
||||
// Gather SNMP settings
|
||||
config = await this.gatherSnmpSettings(config, prompt);
|
||||
|
||||
// Gather threshold settings
|
||||
config = await this.gatherThresholdSettings(config, prompt);
|
||||
|
||||
// Gather UPS model settings
|
||||
config = await this.gatherUpsModelSettings(config, prompt);
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
this.displayConfigSummary(config);
|
||||
|
||||
// Test the connection if requested
|
||||
await this.optionallyTestConnection(config, prompt);
|
||||
|
||||
console.log('\nSetup complete!');
|
||||
await this.optionallyEnableService(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather SNMP settings
|
||||
* @param config Current configuration
|
||||
* @param prompt Function to prompt for user input
|
||||
* @returns Updated configuration
|
||||
*/
|
||||
private async gatherSnmpSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
|
||||
// SNMP IP Address
|
||||
const defaultHost = config.snmp.host;
|
||||
const host = await prompt(`UPS IP Address [${defaultHost}]: `);
|
||||
config.snmp.host = host.trim() || defaultHost;
|
||||
|
||||
// SNMP Port
|
||||
const defaultPort = config.snmp.port;
|
||||
const portInput = await prompt(`SNMP Port [${defaultPort}]: `);
|
||||
const port = parseInt(portInput, 10);
|
||||
config.snmp.port = (portInput.trim() && !isNaN(port)) ? port : defaultPort;
|
||||
|
||||
// SNMP Version
|
||||
const defaultVersion = config.snmp.version;
|
||||
console.log('\nSNMP Version:');
|
||||
console.log(' 1) SNMPv1');
|
||||
console.log(' 2) SNMPv2c');
|
||||
console.log(' 3) SNMPv3 (with security features)');
|
||||
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
|
||||
const version = parseInt(versionInput, 10);
|
||||
config.snmp.version = (versionInput.trim() && (version === 1 || version === 2 || version === 3)) ? version : defaultVersion;
|
||||
|
||||
if (config.snmp.version === 1 || config.snmp.version === 2) {
|
||||
// SNMP Community String (for v1/v2c)
|
||||
const defaultCommunity = config.snmp.community || 'public';
|
||||
const community = await prompt(`SNMP Community String [${defaultCommunity}]: `);
|
||||
config.snmp.community = community.trim() || defaultCommunity;
|
||||
} else if (config.snmp.version === 3) {
|
||||
// SNMP v3 settings
|
||||
config = await this.gatherSnmpV3Settings(config, prompt);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather SNMPv3 specific settings
|
||||
* @param config Current configuration
|
||||
* @param prompt Function to prompt for user input
|
||||
* @returns Updated configuration
|
||||
*/
|
||||
private async gatherSnmpV3Settings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
|
||||
console.log('\nSNMPv3 Security Settings:');
|
||||
|
||||
// Security Level
|
||||
console.log('\nSecurity Level:');
|
||||
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)');
|
||||
console.log(' 2) authNoPriv (Authentication, No Privacy)');
|
||||
console.log(' 3) authPriv (Authentication and Privacy)');
|
||||
const defaultSecLevel = config.snmp.securityLevel ?
|
||||
(config.snmp.securityLevel === 'noAuthNoPriv' ? 1 :
|
||||
config.snmp.securityLevel === 'authNoPriv' ? 2 : 3) : 3;
|
||||
const secLevelInput = await prompt(`Select Security Level [${defaultSecLevel}]: `);
|
||||
const secLevel = parseInt(secLevelInput, 10) || defaultSecLevel;
|
||||
|
||||
if (secLevel === 1) {
|
||||
config.snmp.securityLevel = 'noAuthNoPriv';
|
||||
// No auth, no priv - clear out authentication and privacy settings
|
||||
config.snmp.authProtocol = '';
|
||||
config.snmp.authKey = '';
|
||||
config.snmp.privProtocol = '';
|
||||
config.snmp.privKey = '';
|
||||
// Set appropriate timeout for security level
|
||||
config.snmp.timeout = 5000; // 5 seconds for basic security
|
||||
} else if (secLevel === 2) {
|
||||
config.snmp.securityLevel = 'authNoPriv';
|
||||
// Auth, no priv - clear out privacy settings
|
||||
config.snmp.privProtocol = '';
|
||||
config.snmp.privKey = '';
|
||||
// Set appropriate timeout for security level
|
||||
config.snmp.timeout = 10000; // 10 seconds for authentication
|
||||
} else {
|
||||
config.snmp.securityLevel = 'authPriv';
|
||||
// Set appropriate timeout for security level
|
||||
config.snmp.timeout = 15000; // 15 seconds for full encryption
|
||||
}
|
||||
|
||||
// Username
|
||||
const defaultUsername = config.snmp.username || '';
|
||||
const username = await prompt(`SNMPv3 Username [${defaultUsername}]: `);
|
||||
config.snmp.username = username.trim() || defaultUsername;
|
||||
|
||||
if (secLevel >= 2) {
|
||||
// Authentication settings
|
||||
config = await this.gatherAuthenticationSettings(config, prompt);
|
||||
|
||||
if (secLevel === 3) {
|
||||
// Privacy settings
|
||||
config = await this.gatherPrivacySettings(config, prompt);
|
||||
}
|
||||
|
||||
// Allow customizing the timeout value
|
||||
const defaultTimeout = config.snmp.timeout / 1000; // Convert from ms to seconds for display
|
||||
console.log('\nSNMPv3 operations with authentication and privacy may require longer timeouts.');
|
||||
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
|
||||
const timeout = parseInt(timeoutInput, 10);
|
||||
if (timeoutInput.trim() && !isNaN(timeout)) {
|
||||
config.snmp.timeout = timeout * 1000; // Convert to ms
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather authentication settings for SNMPv3
|
||||
* @param config Current configuration
|
||||
* @param prompt Function to prompt for user input
|
||||
* @returns Updated configuration
|
||||
*/
|
||||
private async gatherAuthenticationSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
|
||||
// Authentication protocol
|
||||
console.log('\nAuthentication Protocol:');
|
||||
console.log(' 1) MD5');
|
||||
console.log(' 2) SHA');
|
||||
const defaultAuthProtocol = config.snmp.authProtocol === 'SHA' ? 2 : 1;
|
||||
const authProtocolInput = await prompt(`Select Authentication Protocol [${defaultAuthProtocol}]: `);
|
||||
const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
|
||||
config.snmp.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
|
||||
|
||||
// Authentication Key/Password
|
||||
const defaultAuthKey = config.snmp.authKey || '';
|
||||
const authKey = await prompt(`Authentication Password ${defaultAuthKey ? '[*****]' : ''}: `);
|
||||
config.snmp.authKey = authKey.trim() || defaultAuthKey;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather privacy settings for SNMPv3
|
||||
* @param config Current configuration
|
||||
* @param prompt Function to prompt for user input
|
||||
* @returns Updated configuration
|
||||
*/
|
||||
private async gatherPrivacySettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
|
||||
// Privacy protocol
|
||||
console.log('\nPrivacy Protocol:');
|
||||
console.log(' 1) DES');
|
||||
console.log(' 2) AES');
|
||||
const defaultPrivProtocol = config.snmp.privProtocol === 'AES' ? 2 : 1;
|
||||
const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
|
||||
const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
|
||||
config.snmp.privProtocol = privProtocol === 2 ? 'AES' : 'DES';
|
||||
|
||||
// Privacy Key/Password
|
||||
const defaultPrivKey = config.snmp.privKey || '';
|
||||
const privKey = await prompt(`Privacy Password ${defaultPrivKey ? '[*****]' : ''}: `);
|
||||
config.snmp.privKey = privKey.trim() || defaultPrivKey;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather threshold settings
|
||||
* @param config Current configuration
|
||||
* @param prompt Function to prompt for user input
|
||||
* @returns Updated configuration
|
||||
*/
|
||||
private async gatherThresholdSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
|
||||
console.log('\nShutdown Thresholds:');
|
||||
|
||||
// Battery threshold
|
||||
const defaultBatteryThreshold = config.thresholds.battery;
|
||||
const batteryThresholdInput = await prompt(`Battery percentage threshold [${defaultBatteryThreshold}%]: `);
|
||||
const batteryThreshold = parseInt(batteryThresholdInput, 10);
|
||||
config.thresholds.battery = (batteryThresholdInput.trim() && !isNaN(batteryThreshold))
|
||||
? batteryThreshold
|
||||
: defaultBatteryThreshold;
|
||||
|
||||
// Runtime threshold
|
||||
const defaultRuntimeThreshold = config.thresholds.runtime;
|
||||
const runtimeThresholdInput = await prompt(`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `);
|
||||
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
|
||||
config.thresholds.runtime = (runtimeThresholdInput.trim() && !isNaN(runtimeThreshold))
|
||||
? runtimeThreshold
|
||||
: defaultRuntimeThreshold;
|
||||
|
||||
// Check interval
|
||||
const defaultInterval = config.checkInterval / 1000; // Convert from ms to seconds for display
|
||||
const intervalInput = await prompt(`Check interval in seconds [${defaultInterval}]: `);
|
||||
const interval = parseInt(intervalInput, 10);
|
||||
config.checkInterval = (intervalInput.trim() && !isNaN(interval))
|
||||
? interval * 1000 // Convert to ms
|
||||
: defaultInterval * 1000;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather UPS model settings
|
||||
* @param config Current configuration
|
||||
* @param prompt Function to prompt for user input
|
||||
* @returns Updated configuration
|
||||
*/
|
||||
private async gatherUpsModelSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
|
||||
console.log('\nUPS Model Selection:');
|
||||
console.log(' 1) CyberPower');
|
||||
console.log(' 2) APC');
|
||||
console.log(' 3) Eaton');
|
||||
console.log(' 4) TrippLite');
|
||||
console.log(' 5) Liebert/Vertiv');
|
||||
console.log(' 6) Custom (Advanced)');
|
||||
|
||||
const defaultModelValue = config.snmp.upsModel === 'cyberpower' ? 1 :
|
||||
config.snmp.upsModel === 'apc' ? 2 :
|
||||
config.snmp.upsModel === 'eaton' ? 3 :
|
||||
config.snmp.upsModel === 'tripplite' ? 4 :
|
||||
config.snmp.upsModel === 'liebert' ? 5 :
|
||||
config.snmp.upsModel === 'custom' ? 6 : 1;
|
||||
|
||||
const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `);
|
||||
const modelValue = parseInt(modelInput, 10) || defaultModelValue;
|
||||
|
||||
if (modelValue === 1) {
|
||||
config.snmp.upsModel = 'cyberpower';
|
||||
} else if (modelValue === 2) {
|
||||
config.snmp.upsModel = 'apc';
|
||||
} else if (modelValue === 3) {
|
||||
config.snmp.upsModel = 'eaton';
|
||||
} else if (modelValue === 4) {
|
||||
config.snmp.upsModel = 'tripplite';
|
||||
} else if (modelValue === 5) {
|
||||
config.snmp.upsModel = 'liebert';
|
||||
} else if (modelValue === 6) {
|
||||
config.snmp.upsModel = 'custom';
|
||||
console.log('\nEnter custom OIDs for your UPS:');
|
||||
console.log('(Leave blank to use standard RFC 1628 OIDs as fallback)');
|
||||
|
||||
// Custom OIDs
|
||||
const powerStatusOID = await prompt('Power Status OID: ');
|
||||
const batteryCapacityOID = await prompt('Battery Capacity OID: ');
|
||||
const batteryRuntimeOID = await prompt('Battery Runtime OID: ');
|
||||
|
||||
// Create custom OIDs object
|
||||
config.snmp.customOIDs = {
|
||||
POWER_STATUS: powerStatusOID.trim(),
|
||||
BATTERY_CAPACITY: batteryCapacityOID.trim(),
|
||||
BATTERY_RUNTIME: batteryRuntimeOID.trim()
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display configuration summary
|
||||
* @param config Current configuration
|
||||
*/
|
||||
private displayConfigSummary(config: any): void {
|
||||
console.log('\n┌─ Configuration Summary ─────────────────┐');
|
||||
console.log(`│ SNMP Host: ${config.snmp.host}:${config.snmp.port}`);
|
||||
console.log(`│ SNMP Version: ${config.snmp.version}`);
|
||||
console.log(`│ UPS Model: ${config.snmp.upsModel}`);
|
||||
console.log(`│ Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`);
|
||||
console.log(`│ Check Interval: ${config.checkInterval/1000} seconds`);
|
||||
console.log('└──────────────────────────────────────────┘\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally test connection to UPS
|
||||
* @param config Current configuration
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async optionallyTestConnection(config: any, prompt: (question: string) => Promise<string>): Promise<void> {
|
||||
const testConnection = await prompt('Would you like to test the connection to your UPS? (y/N): ');
|
||||
if (testConnection.toLowerCase() === 'y') {
|
||||
console.log('\nTesting connection to UPS...');
|
||||
try {
|
||||
// Create a test config with a short timeout
|
||||
const testConfig = {
|
||||
...config.snmp,
|
||||
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for testing
|
||||
};
|
||||
|
||||
const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
|
||||
console.log('\n┌─ Connection Successful! ─────────────────┐');
|
||||
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('└──────────────────────────────────────────┘');
|
||||
} catch (error) {
|
||||
console.error('\n┌─ Connection Failed! ───────────────────────┐');
|
||||
console.error('│ Error: ' + error.message);
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
console.log('\nPlease check your settings and try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally enable systemd service
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async optionallyEnableService(prompt: (question: string) => Promise<string>): Promise<void> {
|
||||
if (process.getuid && process.getuid() !== 0) {
|
||||
console.log('\nNote: Run "sudo nupst enable" to set up NUPST as a system service.');
|
||||
} else {
|
||||
const setupService = await prompt('Would you like to enable NUPST as a system service? (y/N): ');
|
||||
if (setupService.toLowerCase() === 'y') {
|
||||
await this.nupst.getSystemd().install();
|
||||
console.log('Service installed. Use "nupst start" to start the service.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
261
ts/daemon.ts
Normal file
261
ts/daemon.ts
Normal file
@ -0,0 +1,261 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { NupstSnmp, type SnmpConfig } from './snmp.js';
|
||||
|
||||
/**
|
||||
* Configuration interface for the daemon
|
||||
*/
|
||||
export interface NupstConfig {
|
||||
/** SNMP configuration settings */
|
||||
snmp: SnmpConfig;
|
||||
/** Threshold settings for initiating shutdown */
|
||||
thresholds: {
|
||||
/** Shutdown when battery below this percentage */
|
||||
battery: number;
|
||||
/** Shutdown when runtime below this minutes */
|
||||
runtime: number;
|
||||
};
|
||||
/** Check interval in milliseconds */
|
||||
checkInterval: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Daemon class for monitoring UPS and handling shutdown
|
||||
* Responsible for loading/saving config and monitoring the UPS status
|
||||
*/
|
||||
export class NupstDaemon {
|
||||
/** Default configuration path */
|
||||
private readonly CONFIG_PATH = '/etc/nupst/config.json';
|
||||
|
||||
/** Default configuration */
|
||||
private readonly DEFAULT_CONFIG: NupstConfig = {
|
||||
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
|
||||
},
|
||||
checkInterval: 30000, // Check every 30 seconds
|
||||
};
|
||||
|
||||
private config: NupstConfig;
|
||||
private snmp: NupstSnmp;
|
||||
private isRunning: boolean = false;
|
||||
|
||||
/**
|
||||
* Create a new daemon instance with the given SNMP manager
|
||||
*/
|
||||
constructor(snmp: NupstSnmp) {
|
||||
this.snmp = snmp;
|
||||
this.config = this.DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from file
|
||||
* @throws Error if configuration file doesn't exist
|
||||
*/
|
||||
public async loadConfig(): Promise<NupstConfig> {
|
||||
try {
|
||||
// Check if config file exists
|
||||
const configExists = fs.existsSync(this.CONFIG_PATH);
|
||||
if (!configExists) {
|
||||
const errorMsg = `No configuration found at ${this.CONFIG_PATH}`;
|
||||
this.logConfigError(errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
// Read and parse config
|
||||
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
|
||||
this.config = JSON.parse(configData);
|
||||
return this.config;
|
||||
} catch (error) {
|
||||
if (error.message.includes('No configuration found')) {
|
||||
throw error; // Re-throw the no configuration error
|
||||
}
|
||||
|
||||
this.logConfigError(`Error loading configuration: ${error.message}`);
|
||||
throw new Error('Failed to load configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration to file
|
||||
*/
|
||||
public async saveConfig(config: NupstConfig): Promise<void> {
|
||||
try {
|
||||
const configDir = path.dirname(this.CONFIG_PATH);
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||
this.config = config;
|
||||
|
||||
console.log('┌─ Configuration Saved ─────────────────────┐');
|
||||
console.log(`│ Location: ${this.CONFIG_PATH}`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
} catch (error) {
|
||||
console.error('Error saving configuration:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to log configuration errors consistently
|
||||
*/
|
||||
private logConfigError(message: string): void {
|
||||
console.error('┌─ Configuration Error ─────────────────────┐');
|
||||
console.error(`│ ${message}`);
|
||||
console.error('│ Please run \'nupst setup\' first to create a configuration.');
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
public getConfig(): NupstConfig {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SNMP instance
|
||||
*/
|
||||
public getNupstSnmp(): NupstSnmp {
|
||||
return this.snmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the monitoring daemon
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
console.log('Daemon is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Starting NUPST daemon...');
|
||||
|
||||
try {
|
||||
// Load configuration - this will throw an error if config doesn't exist
|
||||
await this.loadConfig();
|
||||
this.logConfigLoaded();
|
||||
|
||||
// Start UPS monitoring
|
||||
this.isRunning = true;
|
||||
await this.monitor();
|
||||
} catch (error) {
|
||||
this.isRunning = false;
|
||||
console.error(`Daemon failed to start: ${error.message}`);
|
||||
process.exit(1); // Exit with error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('└──────────────────────────────────────────┘');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the monitoring daemon
|
||||
*/
|
||||
public stop(): void {
|
||||
console.log('Stopping NUPST daemon...');
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor the UPS status and trigger shutdown when necessary
|
||||
*/
|
||||
private async monitor(): Promise<void> {
|
||||
console.log('Starting UPS monitoring...');
|
||||
|
||||
let lastStatus: 'online' | 'onBattery' | 'unknown' = 'unknown';
|
||||
|
||||
// Monitor continuously
|
||||
while (this.isRunning) {
|
||||
try {
|
||||
const status = await this.snmp.getUpsStatus(this.config.snmp);
|
||||
|
||||
// Log status changes
|
||||
if (status.powerStatus !== lastStatus) {
|
||||
console.log('┌──────────────────────────────────────────┐');
|
||||
console.log(`│ Power status changed: ${lastStatus} → ${status.powerStatus}`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
lastStatus = status.powerStatus;
|
||||
}
|
||||
|
||||
// Handle battery power status
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
await this.handleOnBatteryStatus(status);
|
||||
}
|
||||
|
||||
// Wait before next check
|
||||
await this.sleep(this.config.checkInterval);
|
||||
} catch (error) {
|
||||
console.error('Error during UPS monitoring:', error);
|
||||
await this.sleep(this.config.checkInterval);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('UPS monitoring stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle UPS status when running on battery
|
||||
*/
|
||||
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('└──────────────────────────────────────────┘');
|
||||
|
||||
// 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.snmp.initiateShutdown('Battery capacity below threshold');
|
||||
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.snmp.initiateShutdown('Runtime below threshold');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep for the specified milliseconds
|
||||
*/
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
18
ts/index.ts
Normal file
18
ts/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { NupstCli } from './cli.js';
|
||||
|
||||
/**
|
||||
* Main entry point for NUPST
|
||||
* Initializes the CLI and executes the given command
|
||||
*/
|
||||
async function main() {
|
||||
const cli = new NupstCli();
|
||||
await cli.parseAndExecute(process.argv);
|
||||
}
|
||||
|
||||
// Run the main function and handle any errors
|
||||
main().catch(error => {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
});
|
43
ts/nupst.ts
Normal file
43
ts/nupst.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { NupstSnmp } from './snmp.js';
|
||||
import { NupstDaemon } from './daemon.js';
|
||||
import { NupstSystemd } from './systemd.js';
|
||||
|
||||
/**
|
||||
* Main Nupst class that coordinates all components
|
||||
* Acts as a facade to access SNMP, Daemon, and Systemd functionality
|
||||
*/
|
||||
export class Nupst {
|
||||
private readonly snmp: NupstSnmp;
|
||||
private readonly daemon: NupstDaemon;
|
||||
private readonly systemd: NupstSystemd;
|
||||
|
||||
/**
|
||||
* Create a new Nupst instance with all necessary components
|
||||
*/
|
||||
constructor() {
|
||||
this.snmp = new NupstSnmp();
|
||||
this.daemon = new NupstDaemon(this.snmp);
|
||||
this.systemd = new NupstSystemd(this.daemon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SNMP manager for UPS communication
|
||||
*/
|
||||
public getSnmp(): NupstSnmp {
|
||||
return this.snmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the daemon manager for background monitoring
|
||||
*/
|
||||
public getDaemon(): NupstDaemon {
|
||||
return this.daemon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the systemd manager for service operations
|
||||
*/
|
||||
public getSystemd(): NupstSystemd {
|
||||
return this.systemd;
|
||||
}
|
||||
}
|
6
ts/snmp.ts
Normal file
6
ts/snmp.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Re-export from the snmp module
|
||||
* This file is kept for backward compatibility
|
||||
*/
|
||||
|
||||
export * from './snmp/index.js';
|
98
ts/snmp/encoder.ts
Normal file
98
ts/snmp/encoder.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* SNMP encoding utilities
|
||||
* Contains helper methods for encoding SNMP data
|
||||
*/
|
||||
export class SnmpEncoder {
|
||||
/**
|
||||
* Convert OID string to array of integers
|
||||
* @param oid OID string in dotted notation (e.g. "1.3.6.1.2.1")
|
||||
* @returns Array of integers representing the OID
|
||||
*/
|
||||
public static oidToArray(oid: string): number[] {
|
||||
return oid.split('.').map(n => parseInt(n, 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode an SNMP integer
|
||||
* @param value Integer value to encode
|
||||
* @returns Buffer containing the encoded integer
|
||||
*/
|
||||
public static encodeInteger(value: number): Buffer {
|
||||
const buf = Buffer.alloc(4);
|
||||
buf.writeInt32BE(value, 0);
|
||||
|
||||
// Find first non-zero byte
|
||||
let start = 0;
|
||||
while (start < 3 && buf[start] === 0) {
|
||||
start++;
|
||||
}
|
||||
|
||||
// Handle negative values
|
||||
if (value < 0 && buf[start] === 0) {
|
||||
start--;
|
||||
}
|
||||
|
||||
return buf.slice(start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode an OID
|
||||
* @param oid Array of integers representing the OID
|
||||
* @returns Buffer containing the encoded OID
|
||||
*/
|
||||
public static encodeOID(oid: number[]): Buffer {
|
||||
// First two numbers are encoded as 40*x+y
|
||||
let encodedOid = Buffer.from([40 * (oid[0] || 0) + (oid[1] || 0)]);
|
||||
|
||||
// Encode remaining numbers
|
||||
for (let i = 2; i < oid.length; i++) {
|
||||
const n = oid[i];
|
||||
|
||||
if (n < 128) {
|
||||
// Simple case: number fits in one byte
|
||||
encodedOid = Buffer.concat([encodedOid, Buffer.from([n])]);
|
||||
} else {
|
||||
// Number needs multiple bytes
|
||||
const bytes = [];
|
||||
let value = n;
|
||||
|
||||
// Create bytes array in reverse order
|
||||
do {
|
||||
bytes.unshift(value & 0x7F);
|
||||
value >>= 7;
|
||||
} while (value > 0);
|
||||
|
||||
// Set high bit on all but the last byte
|
||||
for (let j = 0; j < bytes.length - 1; j++) {
|
||||
bytes[j] |= 0x80;
|
||||
}
|
||||
|
||||
encodedOid = Buffer.concat([encodedOid, Buffer.from(bytes)]);
|
||||
}
|
||||
}
|
||||
|
||||
return encodedOid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an ASN.1 integer
|
||||
* @param buffer Buffer containing the encoded integer
|
||||
* @param offset Offset in the buffer
|
||||
* @param length Length of the integer in bytes
|
||||
* @returns Decoded integer value
|
||||
*/
|
||||
public static decodeInteger(buffer: Buffer, offset: number, length: number): number {
|
||||
if (length === 1) {
|
||||
return buffer[offset];
|
||||
} else if (length === 2) {
|
||||
return buffer.readInt16BE(offset);
|
||||
} else if (length === 3) {
|
||||
return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2];
|
||||
} else if (length === 4) {
|
||||
return buffer.readInt32BE(offset);
|
||||
} else {
|
||||
// For longer integers, we'll just return a simple value
|
||||
return buffer[offset];
|
||||
}
|
||||
}
|
||||
}
|
10
ts/snmp/index.ts
Normal file
10
ts/snmp/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Main module entry point for SNMP functionality
|
||||
* Re-exports public types and classes
|
||||
*/
|
||||
|
||||
// Re-export all public types
|
||||
export type { UpsStatus, OIDSet, UpsModel, SnmpConfig } from './types.js';
|
||||
|
||||
// Re-export the SNMP manager class
|
||||
export { NupstSnmp } from './manager.js';
|
514
ts/snmp/manager.ts
Normal file
514
ts/snmp/manager.ts
Normal file
@ -0,0 +1,514 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import * as dgram from 'dgram';
|
||||
import type { OIDSet, SnmpConfig, UpsModel, UpsStatus } 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
|
||||
*/
|
||||
export class NupstSnmp {
|
||||
// Active OID set
|
||||
private activeOIDs: OIDSet;
|
||||
|
||||
// Default SNMP configuration
|
||||
private readonly DEFAULT_CONFIG: SnmpConfig = {
|
||||
host: '127.0.0.1', // Default to localhost
|
||||
port: 161, // Default SNMP port
|
||||
community: 'public', // Default community string for v1/v2c
|
||||
version: 1, // SNMPv1
|
||||
timeout: 5000, // 5 seconds timeout
|
||||
upsModel: 'cyberpower', // Default UPS model
|
||||
};
|
||||
|
||||
// SNMPv3 engine ID and counters
|
||||
private engineID: Buffer = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
|
||||
private engineBoots: number = 0;
|
||||
private engineTime: number = 0;
|
||||
private requestID: number = 1;
|
||||
private debug: boolean = false; // Enable for debug output
|
||||
|
||||
/**
|
||||
* Create a new SNMP manager
|
||||
* @param debug Whether to enable debug mode
|
||||
*/
|
||||
constructor(debug = false) {
|
||||
this.debug = debug;
|
||||
// Set default OID set
|
||||
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set active OID set based on UPS model
|
||||
* @param config SNMP configuration
|
||||
*/
|
||||
private setActiveOIDs(config: SnmpConfig): void {
|
||||
// If custom OIDs are provided, use them
|
||||
if (config.upsModel === 'custom' && config.customOIDs) {
|
||||
this.activeOIDs = config.customOIDs;
|
||||
if (this.debug) {
|
||||
console.log('Using custom OIDs:', this.activeOIDs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Use OIDs for the specified UPS model or default to Cyberpower
|
||||
const model = config.upsModel || 'cyberpower';
|
||||
this.activeOIDs = UpsOidSets.getOidSet(model);
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Using OIDs for UPS model: ${model}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable debug mode
|
||||
*/
|
||||
public enableDebug(): void {
|
||||
this.debug = true;
|
||||
console.log('SNMP debug mode enabled - detailed logs will be shown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an SNMP GET request
|
||||
* @param oid OID to query
|
||||
* @param config SNMP configuration
|
||||
* @returns Promise resolving to the SNMP response value
|
||||
*/
|
||||
public async snmpGet(oid: string, config = this.DEFAULT_CONFIG): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = dgram.createSocket('udp4');
|
||||
|
||||
// Create appropriate request based on SNMP version
|
||||
let request: Buffer;
|
||||
if (config.version === 3) {
|
||||
request = SnmpPacketCreator.createSnmpV3GetRequest(
|
||||
oid,
|
||||
config,
|
||||
this.engineID,
|
||||
this.engineBoots,
|
||||
this.engineTime,
|
||||
this.requestID++,
|
||||
this.debug
|
||||
);
|
||||
} else {
|
||||
request = SnmpPacketCreator.createSnmpGetRequest(oid, config.community || 'public', this.debug);
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Sending SNMP ${config.version === 3 ? 'v3' : ('v' + config.version)} request to ${config.host}:${config.port}`);
|
||||
console.log('Request length:', request.length);
|
||||
console.log('First 16 bytes of request:', request.slice(0, 16).toString('hex'));
|
||||
console.log('Full request hex:', request.toString('hex'));
|
||||
}
|
||||
|
||||
// Set timeout - add extra logging for debugging
|
||||
const timeout = setTimeout(() => {
|
||||
socket.close();
|
||||
if (this.debug) {
|
||||
console.error('---------------------------------------');
|
||||
console.error('SNMP request timed out after', config.timeout, 'ms');
|
||||
console.error('SNMP Version:', config.version);
|
||||
if (config.version === 3) {
|
||||
console.error('SNMPv3 Security Level:', config.securityLevel);
|
||||
console.error('SNMPv3 Username:', config.username);
|
||||
console.error('SNMPv3 Auth Protocol:', config.authProtocol || 'None');
|
||||
console.error('SNMPv3 Privacy Protocol:', config.privProtocol || 'None');
|
||||
}
|
||||
console.error('OID:', oid);
|
||||
console.error('Host:', config.host);
|
||||
console.error('Port:', config.port);
|
||||
console.error('---------------------------------------');
|
||||
}
|
||||
reject(new Error(`SNMP request timed out after ${config.timeout}ms`));
|
||||
}, config.timeout);
|
||||
|
||||
// Listen for responses
|
||||
socket.on('message', (message, rinfo) => {
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Received SNMP response from ${rinfo.address}:${rinfo.port}`);
|
||||
console.log('Response length:', message.length);
|
||||
console.log('First 16 bytes of response:', message.slice(0, 16).toString('hex'));
|
||||
console.log('Full response hex:', message.toString('hex'));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = SnmpPacketParser.parseSnmpResponse(message, config, this.debug);
|
||||
|
||||
if (this.debug) {
|
||||
console.log('Parsed SNMP response:', result);
|
||||
}
|
||||
|
||||
socket.close();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error('Error parsing SNMP response:', error);
|
||||
}
|
||||
socket.close();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
socket.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
socket.close();
|
||||
if (this.debug) {
|
||||
console.error('Socket error during SNMP request:', error);
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// First send the request directly without binding to a specific port
|
||||
// This lets the OS pick an available port instead of trying to bind to one
|
||||
socket.send(request, 0, request.length, config.port, config.host, (error) => {
|
||||
if (error) {
|
||||
clearTimeout(timeout);
|
||||
socket.close();
|
||||
if (this.debug) {
|
||||
console.error('Error sending SNMP request:', error);
|
||||
}
|
||||
reject(error);
|
||||
} else if (this.debug) {
|
||||
console.log('SNMP request sent successfully');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of the UPS
|
||||
* @param config SNMP configuration
|
||||
* @returns Promise resolving to the UPS status
|
||||
*/
|
||||
public async getUpsStatus(config = this.DEFAULT_CONFIG): Promise<UpsStatus> {
|
||||
try {
|
||||
// Set active OID set based on UPS model in config
|
||||
this.setActiveOIDs(config);
|
||||
|
||||
if (this.debug) {
|
||||
console.log('---------------------------------------');
|
||||
console.log('Getting UPS status with config:');
|
||||
console.log(' Host:', config.host);
|
||||
console.log(' Port:', config.port);
|
||||
console.log(' Version:', config.version);
|
||||
console.log(' Timeout:', config.timeout, 'ms');
|
||||
console.log(' UPS Model:', config.upsModel || 'cyberpower');
|
||||
if (config.version === 1 || config.version === 2) {
|
||||
console.log(' Community:', config.community);
|
||||
} else if (config.version === 3) {
|
||||
console.log(' Security Level:', config.securityLevel);
|
||||
console.log(' Username:', config.username);
|
||||
console.log(' Auth Protocol:', config.authProtocol || 'None');
|
||||
console.log(' Privacy Protocol:', config.privProtocol || 'None');
|
||||
}
|
||||
console.log('Using OIDs:');
|
||||
console.log(' Power Status:', this.activeOIDs.POWER_STATUS);
|
||||
console.log(' Battery Capacity:', this.activeOIDs.BATTERY_CAPACITY);
|
||||
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
||||
console.log('---------------------------------------');
|
||||
}
|
||||
|
||||
// For SNMPv3, we need to discover the engine ID first
|
||||
if (config.version === 3) {
|
||||
if (this.debug) {
|
||||
console.log('SNMPv3 detected, starting engine ID discovery');
|
||||
}
|
||||
|
||||
try {
|
||||
const discoveredEngineId = await this.discoverEngineId(config);
|
||||
if (discoveredEngineId) {
|
||||
this.engineID = discoveredEngineId;
|
||||
if (this.debug) {
|
||||
console.log('Using discovered engine ID:', this.engineID.toString('hex'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.warn('Engine ID discovery failed, using default:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get SNMP value with retry
|
||||
const getSNMPValueWithRetry = async (oid: string, description: string) => {
|
||||
if (oid === '') {
|
||||
if (this.debug) {
|
||||
console.log(`No OID provided for ${description}, skipping`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Getting ${description} OID: ${oid}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const value = await this.snmpGet(oid, config);
|
||||
if (this.debug) {
|
||||
console.log(`${description} value:`, value);
|
||||
}
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error(`Error getting ${description}:`, error.message);
|
||||
}
|
||||
|
||||
// If we got a timeout and it's SNMPv3, try with different security levels
|
||||
if (error.message.includes('timed out') && config.version === 3) {
|
||||
if (this.debug) {
|
||||
console.log(`Retrying ${description} with fallback settings...`);
|
||||
}
|
||||
|
||||
// Create a retry config with lower security level
|
||||
if (config.securityLevel === 'authPriv') {
|
||||
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
|
||||
try {
|
||||
if (this.debug) {
|
||||
console.log(`Retrying with authNoPriv security level`);
|
||||
}
|
||||
const value = await this.snmpGet(oid, retryConfig);
|
||||
if (this.debug) {
|
||||
console.log(`${description} retry value:`, value);
|
||||
}
|
||||
return value;
|
||||
} catch (retryError) {
|
||||
if (this.debug) {
|
||||
console.error(`Retry failed for ${description}:`, retryError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're still having trouble, try with standard OIDs
|
||||
if (config.upsModel !== 'custom') {
|
||||
try {
|
||||
// Try RFC 1628 standard UPS MIB OIDs
|
||||
const standardOIDs = UpsOidSets.getStandardOids();
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`);
|
||||
}
|
||||
|
||||
const standardValue = await this.snmpGet(standardOIDs[description], config);
|
||||
if (this.debug) {
|
||||
console.log(`${description} standard OID value:`, standardValue);
|
||||
}
|
||||
return standardValue;
|
||||
} catch (stdError) {
|
||||
if (this.debug) {
|
||||
console.error(`Standard OID retry failed for ${description}:`, stdError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return a default value if all attempts fail
|
||||
if (this.debug) {
|
||||
console.log(`Using default value 0 for ${description}`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Get all values with independent retry logic
|
||||
const powerStatusValue = await getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status');
|
||||
const batteryCapacity = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity') || 0;
|
||||
const batteryRuntime = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime') || 0;
|
||||
|
||||
// Determine power status - handle different values for different UPS models
|
||||
let powerStatus: 'online' | 'onBattery' | 'unknown' = 'unknown';
|
||||
|
||||
// Different UPS models use different values for power status
|
||||
if (config.upsModel === 'cyberpower') {
|
||||
// CyberPower RMCARD205: upsBaseOutputStatus values
|
||||
// 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc.
|
||||
if (powerStatusValue === 2) {
|
||||
powerStatus = 'online';
|
||||
} else if (powerStatusValue === 3) {
|
||||
powerStatus = 'onBattery';
|
||||
}
|
||||
} else {
|
||||
// Default interpretation for other UPS models
|
||||
if (powerStatusValue === 1) {
|
||||
powerStatus = 'online';
|
||||
} else if (powerStatusValue === 2) {
|
||||
powerStatus = 'onBattery';
|
||||
}
|
||||
}
|
||||
|
||||
// Convert TimeTicks to minutes for CyberPower runtime (value is in 1/100 seconds)
|
||||
let processedRuntime = batteryRuntime;
|
||||
if (config.upsModel === 'cyberpower' && batteryRuntime > 0) {
|
||||
// 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`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
powerStatus,
|
||||
batteryCapacity,
|
||||
batteryRuntime: processedRuntime,
|
||||
raw: {
|
||||
powerStatus: powerStatusValue,
|
||||
batteryCapacity,
|
||||
batteryRuntime,
|
||||
},
|
||||
};
|
||||
|
||||
if (this.debug) {
|
||||
console.log('---------------------------------------');
|
||||
console.log('UPS status result:');
|
||||
console.log(' Power Status:', result.powerStatus);
|
||||
console.log(' Battery Capacity:', result.batteryCapacity + '%');
|
||||
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
||||
console.log('---------------------------------------');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error('---------------------------------------');
|
||||
console.error('Error getting UPS status:', error.message);
|
||||
console.error('---------------------------------------');
|
||||
}
|
||||
throw new Error(`Failed to get UPS status: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover SNMP engine ID (for SNMPv3)
|
||||
* Sends a proper discovery message to get the engine ID from the device
|
||||
* @param config SNMP configuration
|
||||
* @returns Promise resolving to the discovered engine ID
|
||||
*/
|
||||
public async discoverEngineId(config: SnmpConfig): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = dgram.createSocket('udp4');
|
||||
|
||||
// Create a proper discovery message (SNMPv3 with noAuthNoPriv)
|
||||
const discoveryConfig: SnmpConfig = {
|
||||
...config,
|
||||
securityLevel: 'noAuthNoPriv',
|
||||
username: '', // Empty username for discovery
|
||||
};
|
||||
|
||||
// Create a simple GetRequest for sysDescr (a commonly available OID)
|
||||
const request = SnmpPacketCreator.createDiscoveryMessage(discoveryConfig, this.requestID++);
|
||||
|
||||
if (this.debug) {
|
||||
console.log('Sending SNMPv3 discovery message');
|
||||
console.log('SNMPv3 Discovery message:', request.toString('hex'));
|
||||
}
|
||||
|
||||
// Set timeout - use a longer timeout for discovery phase
|
||||
const discoveryTimeout = Math.max(config.timeout, 15000); // At least 15 seconds for discovery
|
||||
const timeout = setTimeout(() => {
|
||||
socket.close();
|
||||
// Fall back to default engine ID if discovery fails
|
||||
if (this.debug) {
|
||||
console.error('---------------------------------------');
|
||||
console.error('Engine ID discovery timed out after', discoveryTimeout, 'ms');
|
||||
console.error('SNMPv3 settings:');
|
||||
console.error(' Username:', config.username);
|
||||
console.error(' Security Level:', config.securityLevel);
|
||||
console.error(' Host:', config.host);
|
||||
console.error(' Port:', config.port);
|
||||
console.error('Using default engine ID:', this.engineID.toString('hex'));
|
||||
console.error('---------------------------------------');
|
||||
}
|
||||
resolve(this.engineID);
|
||||
}, discoveryTimeout);
|
||||
|
||||
// Listen for responses
|
||||
socket.on('message', (message, rinfo) => {
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Received SNMPv3 discovery response from ${rinfo.address}:${rinfo.port}`);
|
||||
console.log('Response:', message.toString('hex'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract engine ID from response
|
||||
const engineId = SnmpPacketParser.extractEngineId(message, this.debug);
|
||||
if (engineId) {
|
||||
this.engineID = engineId; // Update the engine ID
|
||||
if (this.debug) {
|
||||
console.log('Discovered engine ID:', engineId.toString('hex'));
|
||||
}
|
||||
socket.close();
|
||||
resolve(engineId);
|
||||
} else {
|
||||
if (this.debug) {
|
||||
console.log('Could not extract engine ID, using default');
|
||||
}
|
||||
socket.close();
|
||||
resolve(this.engineID);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error('Error extracting engine ID:', error);
|
||||
}
|
||||
socket.close();
|
||||
resolve(this.engineID); // Fall back to default engine ID
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
socket.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
socket.close();
|
||||
if (this.debug) {
|
||||
console.error('Engine ID discovery socket error:', error);
|
||||
}
|
||||
resolve(this.engineID); // Fall back to default engine ID
|
||||
});
|
||||
|
||||
// Send request directly without binding
|
||||
socket.send(request, 0, request.length, config.port, config.host, (error) => {
|
||||
if (error) {
|
||||
clearTimeout(timeout);
|
||||
socket.close();
|
||||
if (this.debug) {
|
||||
console.error('Error sending discovery message:', error);
|
||||
}
|
||||
resolve(this.engineID); // Fall back to default engine ID
|
||||
} else if (this.debug) {
|
||||
console.log('Discovery message sent successfully');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const { stdout } = await execAsync('shutdown -h +1 "UPS battery critical, shutting down in 1 minute"');
|
||||
console.log('Shutdown initiated:', stdout);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
75
ts/snmp/oid-sets.ts
Normal file
75
ts/snmp/oid-sets.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import type { OIDSet, UpsModel } from './types.js';
|
||||
|
||||
/**
|
||||
* OID sets for different UPS models
|
||||
* Contains mappings between UPS models and their SNMP OIDs
|
||||
*/
|
||||
export class UpsOidSets {
|
||||
/**
|
||||
* OID sets for different UPS models
|
||||
*/
|
||||
private static readonly UPS_OID_SETS: Record<UpsModel, OIDSet> = {
|
||||
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
|
||||
cyberpower: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus (2=online, 3=on battery)
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
||||
},
|
||||
|
||||
// APC OIDs
|
||||
apc: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery)
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
||||
},
|
||||
|
||||
// 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
|
||||
},
|
||||
|
||||
// TrippLite OIDs
|
||||
tripplite: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
||||
},
|
||||
|
||||
// Liebert/Vertiv OIDs
|
||||
liebert: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
||||
},
|
||||
|
||||
// Custom OIDs (to be provided by the user)
|
||||
custom: {
|
||||
POWER_STATUS: '',
|
||||
BATTERY_CAPACITY: '',
|
||||
BATTERY_RUNTIME: '',
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get OID set for a specific UPS model
|
||||
* @param model UPS model name
|
||||
* @returns OID set for the model
|
||||
*/
|
||||
public static getOidSet(model: UpsModel): OIDSet {
|
||||
return this.UPS_OID_SETS[model];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get standard RFC 1628 OID set as fallback
|
||||
* @returns Standard OID set
|
||||
*/
|
||||
public static getStandardOids(): Record<string, string> {
|
||||
return {
|
||||
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
||||
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
||||
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0' // upsEstimatedMinutesRemaining
|
||||
};
|
||||
}
|
||||
}
|
651
ts/snmp/packet-creator.ts
Normal file
651
ts/snmp/packet-creator.ts
Normal file
@ -0,0 +1,651 @@
|
||||
import * as crypto from 'crypto';
|
||||
import type { SnmpConfig, SnmpV3SecurityParams } from './types.js';
|
||||
import { SnmpEncoder } from './encoder.js';
|
||||
|
||||
/**
|
||||
* SNMP packet creation utilities
|
||||
* Creates SNMP request packets for different SNMP versions
|
||||
*/
|
||||
export class SnmpPacketCreator {
|
||||
/**
|
||||
* Create an SNMPv1 GET request
|
||||
* @param oid OID to query
|
||||
* @param community Community string
|
||||
* @param debug Whether to enable debug output
|
||||
* @returns Buffer containing the SNMP request
|
||||
*/
|
||||
public static createSnmpGetRequest(oid: string, community: string, debug: boolean = false): Buffer {
|
||||
const oidArray = SnmpEncoder.oidToArray(oid);
|
||||
const encodedOid = SnmpEncoder.encodeOID(oidArray);
|
||||
|
||||
if (debug) {
|
||||
console.log('OID array length:', oidArray.length);
|
||||
console.log('OID array:', oidArray);
|
||||
}
|
||||
|
||||
// SNMP message structure
|
||||
// Sequence
|
||||
// Version (Integer)
|
||||
// Community (String)
|
||||
// PDU (GetRequest)
|
||||
// Request ID (Integer)
|
||||
// Error Status (Integer)
|
||||
// Error Index (Integer)
|
||||
// Variable Bindings (Sequence)
|
||||
// Variable (Sequence)
|
||||
// OID (ObjectIdentifier)
|
||||
// Value (Null)
|
||||
|
||||
// Use the standard method from our test that is known to work
|
||||
// Create a fixed request ID (0x00000001) to ensure deterministic behavior
|
||||
const requestId = Buffer.from([0x00, 0x00, 0x00, 0x01]);
|
||||
|
||||
// Encode values
|
||||
const versionBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x00]) // SNMP version 1 (0)
|
||||
]);
|
||||
|
||||
const communityBuf = Buffer.concat([
|
||||
Buffer.from([0x04, community.length]), // ASN.1 Octet String, length
|
||||
Buffer.from(community) // Community string
|
||||
]);
|
||||
|
||||
const requestIdBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
||||
requestId // Fixed Request ID
|
||||
]);
|
||||
|
||||
const errorStatusBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x00]) // Error Status (0 = no error)
|
||||
]);
|
||||
|
||||
const errorIndexBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x00]) // Error Index (0)
|
||||
]);
|
||||
|
||||
const oidValueBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([encodedOid.length + 2]), // Length
|
||||
Buffer.from([0x06]), // ASN.1 Object Identifier
|
||||
Buffer.from([encodedOid.length]), // Length
|
||||
encodedOid, // OID
|
||||
Buffer.from([0x05, 0x00]) // Null value
|
||||
]);
|
||||
|
||||
const varBindingsBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([oidValueBuf.length]), // Length
|
||||
oidValueBuf // Variable binding
|
||||
]);
|
||||
|
||||
const pduBuf = Buffer.concat([
|
||||
Buffer.from([0xa0]), // ASN.1 Context-specific Constructed 0 (GetRequest)
|
||||
Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length
|
||||
requestIdBuf, // Request ID
|
||||
errorStatusBuf, // Error Status
|
||||
errorIndexBuf, // Error Index
|
||||
varBindingsBuf // Variable Bindings
|
||||
]);
|
||||
|
||||
const messageBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([versionBuf.length + communityBuf.length + pduBuf.length]), // Length
|
||||
versionBuf, // Version
|
||||
communityBuf, // Community
|
||||
pduBuf // PDU
|
||||
]);
|
||||
|
||||
if (debug) {
|
||||
console.log('SNMP Request buffer:', messageBuf.toString('hex'));
|
||||
}
|
||||
|
||||
return messageBuf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an SNMPv3 GET request
|
||||
* @param oid OID to query
|
||||
* @param config SNMP configuration
|
||||
* @param engineID Engine ID
|
||||
* @param engineBoots Engine boots counter
|
||||
* @param engineTime Engine time counter
|
||||
* @param requestID Request ID
|
||||
* @param debug Whether to enable debug output
|
||||
* @returns Buffer containing the SNMP request
|
||||
*/
|
||||
public static createSnmpV3GetRequest(
|
||||
oid: string,
|
||||
config: SnmpConfig,
|
||||
engineID: Buffer,
|
||||
engineBoots: number,
|
||||
engineTime: number,
|
||||
requestID: number,
|
||||
debug: boolean = false
|
||||
): Buffer {
|
||||
if (debug) {
|
||||
console.log('Creating SNMPv3 GET request for OID:', oid);
|
||||
console.log('With config:', {
|
||||
...config,
|
||||
authKey: config.authKey ? '***' : undefined,
|
||||
privKey: config.privKey ? '***' : undefined
|
||||
});
|
||||
}
|
||||
|
||||
const oidArray = SnmpEncoder.oidToArray(oid);
|
||||
const encodedOid = SnmpEncoder.encodeOID(oidArray);
|
||||
|
||||
if (debug) {
|
||||
console.log('Using engine ID:', engineID.toString('hex'));
|
||||
console.log('Engine boots:', engineBoots);
|
||||
console.log('Engine time:', engineTime);
|
||||
console.log('Request ID:', requestID);
|
||||
}
|
||||
|
||||
// Create security parameters
|
||||
const securityParams: SnmpV3SecurityParams = {
|
||||
msgAuthoritativeEngineID: engineID,
|
||||
msgAuthoritativeEngineBoots: engineBoots,
|
||||
msgAuthoritativeEngineTime: engineTime,
|
||||
msgUserName: config.username || '',
|
||||
msgAuthenticationParameters: Buffer.alloc(12, 0), // Will be filled in later for auth
|
||||
msgPrivacyParameters: Buffer.alloc(8, 0), // For privacy
|
||||
};
|
||||
|
||||
// Create the PDU (Protocol Data Unit)
|
||||
// This is wrapped within the security parameters
|
||||
const requestIdBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
||||
SnmpEncoder.encodeInteger(requestID) // Request ID
|
||||
]);
|
||||
|
||||
const errorStatusBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x00]) // Error Status (0 = no error)
|
||||
]);
|
||||
|
||||
const errorIndexBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x00]) // Error Index (0)
|
||||
]);
|
||||
|
||||
const oidValueBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([encodedOid.length + 2]), // Length
|
||||
Buffer.from([0x06]), // ASN.1 Object Identifier
|
||||
Buffer.from([encodedOid.length]), // Length
|
||||
encodedOid, // OID
|
||||
Buffer.from([0x05, 0x00]) // Null value
|
||||
]);
|
||||
|
||||
const varBindingsBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([oidValueBuf.length]), // Length
|
||||
oidValueBuf // Variable binding
|
||||
]);
|
||||
|
||||
const pduBuf = Buffer.concat([
|
||||
Buffer.from([0xa0]), // ASN.1 Context-specific Constructed 0 (GetRequest)
|
||||
Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length
|
||||
requestIdBuf, // Request ID
|
||||
errorStatusBuf, // Error Status
|
||||
errorIndexBuf, // Error Index
|
||||
varBindingsBuf // Variable Bindings
|
||||
]);
|
||||
|
||||
// Create the security parameters
|
||||
const engineIdBuf = Buffer.concat([
|
||||
Buffer.from([0x04, securityParams.msgAuthoritativeEngineID.length]), // ASN.1 Octet String
|
||||
securityParams.msgAuthoritativeEngineID
|
||||
]);
|
||||
|
||||
const engineBootsBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
||||
SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineBoots)
|
||||
]);
|
||||
|
||||
const engineTimeBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
||||
SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineTime)
|
||||
]);
|
||||
|
||||
const userNameBuf = Buffer.concat([
|
||||
Buffer.from([0x04, securityParams.msgUserName.length]), // ASN.1 Octet String
|
||||
Buffer.from(securityParams.msgUserName)
|
||||
]);
|
||||
|
||||
const authParamsBuf = Buffer.concat([
|
||||
Buffer.from([0x04, securityParams.msgAuthenticationParameters.length]), // ASN.1 Octet String
|
||||
securityParams.msgAuthenticationParameters
|
||||
]);
|
||||
|
||||
const privParamsBuf = Buffer.concat([
|
||||
Buffer.from([0x04, securityParams.msgPrivacyParameters.length]), // ASN.1 Octet String
|
||||
securityParams.msgPrivacyParameters
|
||||
]);
|
||||
|
||||
// Security parameters sequence
|
||||
const securityParamsBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([engineIdBuf.length + engineBootsBuf.length + engineTimeBuf.length +
|
||||
userNameBuf.length + authParamsBuf.length + privParamsBuf.length]), // Length
|
||||
engineIdBuf,
|
||||
engineBootsBuf,
|
||||
engineTimeBuf,
|
||||
userNameBuf,
|
||||
authParamsBuf,
|
||||
privParamsBuf
|
||||
]);
|
||||
|
||||
// Determine security level flags
|
||||
let securityFlags = 0;
|
||||
if (config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') {
|
||||
securityFlags |= 0x01; // Authentication flag
|
||||
}
|
||||
if (config.securityLevel === 'authPriv') {
|
||||
securityFlags |= 0x02; // Privacy flag
|
||||
}
|
||||
|
||||
// Set reportable flag - required for SNMPv3
|
||||
securityFlags |= 0x04; // Reportable flag
|
||||
|
||||
// Create SNMPv3 header
|
||||
const msgIdBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
||||
SnmpEncoder.encodeInteger(requestID) // Message ID (same as request ID for simplicity)
|
||||
]);
|
||||
|
||||
const msgMaxSizeBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
||||
SnmpEncoder.encodeInteger(65507) // Max message size
|
||||
]);
|
||||
|
||||
const msgFlagsBuf = Buffer.concat([
|
||||
Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1
|
||||
Buffer.from([securityFlags])
|
||||
]);
|
||||
|
||||
const msgSecModelBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x03]) // Security model (3 = USM)
|
||||
]);
|
||||
|
||||
// SNMPv3 header
|
||||
const msgHeaderBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length
|
||||
msgIdBuf,
|
||||
msgMaxSizeBuf,
|
||||
msgFlagsBuf,
|
||||
msgSecModelBuf
|
||||
]);
|
||||
|
||||
// SNMPv3 security parameters
|
||||
const msgSecurityBuf = Buffer.concat([
|
||||
Buffer.from([0x04]), // ASN.1 Octet String
|
||||
Buffer.from([securityParamsBuf.length]), // Length
|
||||
securityParamsBuf
|
||||
]);
|
||||
|
||||
// Create scopedPDU
|
||||
// In SNMPv3, the PDU is wrapped in a "scoped PDU" structure
|
||||
const contextEngineBuf = Buffer.concat([
|
||||
Buffer.from([0x04, engineID.length]), // ASN.1 Octet String
|
||||
engineID
|
||||
]);
|
||||
|
||||
const contextNameBuf = Buffer.concat([
|
||||
Buffer.from([0x04, 0x00]), // ASN.1 Octet String, length 0 (empty context name)
|
||||
]);
|
||||
|
||||
const scopedPduBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]), // Length
|
||||
contextEngineBuf,
|
||||
contextNameBuf,
|
||||
pduBuf
|
||||
]);
|
||||
|
||||
// For authPriv, we need to encrypt the scopedPDU
|
||||
let encryptedPdu = scopedPduBuf;
|
||||
if (config.securityLevel === 'authPriv' && config.privKey) {
|
||||
// In a real implementation, encryption would be applied here
|
||||
// For this example, we'll just simulate it
|
||||
encryptedPdu = this.simulateEncryption(scopedPduBuf, config);
|
||||
}
|
||||
|
||||
// Final scopedPDU (encrypted or not)
|
||||
const finalScopedPduBuf = Buffer.concat([
|
||||
Buffer.from([0x04]), // ASN.1 Octet String
|
||||
Buffer.from([encryptedPdu.length]), // Length
|
||||
encryptedPdu
|
||||
]);
|
||||
|
||||
// Combine everything for the final message
|
||||
const versionBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x03]) // SNMP version 3 (3)
|
||||
]);
|
||||
|
||||
const messageBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([versionBuf.length + msgHeaderBuf.length + msgSecurityBuf.length + finalScopedPduBuf.length]), // Length
|
||||
versionBuf,
|
||||
msgHeaderBuf,
|
||||
msgSecurityBuf,
|
||||
finalScopedPduBuf
|
||||
]);
|
||||
|
||||
// If using authentication, calculate and insert the authentication parameters
|
||||
if ((config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') &&
|
||||
config.authKey && config.authProtocol) {
|
||||
const authenticatedMsg = this.addAuthentication(messageBuf, config, authParamsBuf);
|
||||
|
||||
if (debug) {
|
||||
console.log('Created authenticated SNMPv3 message');
|
||||
console.log('Final message length:', authenticatedMsg.length);
|
||||
}
|
||||
|
||||
return authenticatedMsg;
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log('Created SNMPv3 message without authentication');
|
||||
console.log('Final message length:', messageBuf.length);
|
||||
}
|
||||
|
||||
return messageBuf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate encryption for authPriv security level
|
||||
* In a real implementation, this would use the specified privacy protocol (DES/AES)
|
||||
* @param data Data to encrypt
|
||||
* @param config SNMP configuration
|
||||
* @returns Encrypted data
|
||||
*/
|
||||
private static simulateEncryption(data: Buffer, config: SnmpConfig): Buffer {
|
||||
// This is a placeholder - in a real implementation, you would:
|
||||
// 1. Generate an initialization vector (IV)
|
||||
// 2. Use the privacy key derived from the privKey
|
||||
// 3. Apply the appropriate encryption algorithm (DES/AES)
|
||||
|
||||
// For demonstration purposes only
|
||||
if (config.privProtocol === 'AES' && config.privKey) {
|
||||
try {
|
||||
// Create a deterministic IV for demo purposes (not secure for production)
|
||||
const iv = Buffer.alloc(16, 0);
|
||||
const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
iv[i] = engineID[i % engineID.length];
|
||||
}
|
||||
|
||||
// Create a key from the privKey (proper key localization should be used in production)
|
||||
const key = crypto.createHash('md5').update(config.privKey).digest();
|
||||
|
||||
// Create cipher and encrypt
|
||||
const cipher = crypto.createCipheriv('aes-128-cfb', key, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||
|
||||
return encrypted;
|
||||
} catch (error) {
|
||||
console.warn('AES encryption failed, falling back to plaintext:', error);
|
||||
return data;
|
||||
}
|
||||
} else if (config.privProtocol === 'DES' && config.privKey) {
|
||||
try {
|
||||
// Create a deterministic IV for demo purposes (not secure for production)
|
||||
const iv = Buffer.alloc(8, 0);
|
||||
const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
iv[i] = engineID[i % engineID.length];
|
||||
}
|
||||
|
||||
// Create a key from the privKey (proper key localization should be used in production)
|
||||
const key = crypto.createHash('md5').update(config.privKey).digest().slice(0, 8);
|
||||
|
||||
// Create cipher and encrypt
|
||||
const cipher = crypto.createCipheriv('des-cbc', key, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||
|
||||
return encrypted;
|
||||
} catch (error) {
|
||||
console.warn('DES encryption failed, falling back to plaintext:', error);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
return data; // Return unencrypted data as fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Add authentication to SNMPv3 message
|
||||
* @param message Message to authenticate
|
||||
* @param config SNMP configuration
|
||||
* @param authParamsBuf Authentication parameters buffer
|
||||
* @returns Authenticated message
|
||||
*/
|
||||
private static addAuthentication(message: Buffer, config: SnmpConfig, authParamsBuf: Buffer): Buffer {
|
||||
// In a real implementation, this would:
|
||||
// 1. Zero out the authentication parameters field
|
||||
// 2. Calculate HMAC-MD5 or HMAC-SHA1 over the entire message
|
||||
// 3. Insert the HMAC into the authentication parameters field
|
||||
|
||||
if (!config.authKey) {
|
||||
return message;
|
||||
}
|
||||
|
||||
try {
|
||||
// Find position of auth parameters in the message
|
||||
// This is a more reliable way to find the exact position
|
||||
let authParamsPos = -1;
|
||||
for (let i = 0; i < message.length - 16; i++) {
|
||||
// Look for the auth params pattern: 0x04 0x0C 0x00 0x00...
|
||||
if (message[i] === 0x04 && message[i + 1] === 0x0C) {
|
||||
// Check if next 12 bytes are all zeros
|
||||
let allZeros = true;
|
||||
for (let j = 0; j < 12; j++) {
|
||||
if (message[i + 2 + j] !== 0) {
|
||||
allZeros = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allZeros) {
|
||||
authParamsPos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authParamsPos === -1) {
|
||||
return message;
|
||||
}
|
||||
|
||||
// Create a copy of the message with zeroed auth parameters
|
||||
const msgCopy = Buffer.from(message);
|
||||
|
||||
// Prepare the authentication key according to RFC3414
|
||||
// We should use the standard key localization process
|
||||
const localizedKey = this.localizeAuthKey(config.authKey,
|
||||
Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]),
|
||||
config.authProtocol);
|
||||
|
||||
// Calculate HMAC
|
||||
let hmac;
|
||||
if (config.authProtocol === 'SHA') {
|
||||
hmac = crypto.createHmac('sha1', localizedKey).update(msgCopy).digest().slice(0, 12);
|
||||
} else {
|
||||
// Default to MD5
|
||||
hmac = crypto.createHmac('md5', localizedKey).update(msgCopy).digest().slice(0, 12);
|
||||
}
|
||||
|
||||
// Copy HMAC into original message
|
||||
hmac.copy(message, authParamsPos + 2);
|
||||
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.warn('Authentication failed:', error);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Localize authentication key according to RFC3414
|
||||
* @param key Authentication key
|
||||
* @param engineId Engine ID
|
||||
* @param authProtocol Authentication protocol
|
||||
* @returns Localized key
|
||||
*/
|
||||
private static localizeAuthKey(key: string, engineId: Buffer, authProtocol: string = 'MD5'): Buffer {
|
||||
try {
|
||||
// Convert password to key using hash
|
||||
let initialHash;
|
||||
if (authProtocol === 'SHA') {
|
||||
initialHash = crypto.createHash('sha1');
|
||||
} else {
|
||||
initialHash = crypto.createHash('md5');
|
||||
}
|
||||
|
||||
// Generate the initial key - repeated hashing of password + padding
|
||||
const password = Buffer.from(key);
|
||||
let passwordIndex = 0;
|
||||
|
||||
// Create a buffer of 1MB (1048576 bytes) filled with the password
|
||||
const buffer = Buffer.alloc(1048576);
|
||||
for (let i = 0; i < 1048576; i++) {
|
||||
buffer[i] = password[passwordIndex];
|
||||
passwordIndex = (passwordIndex + 1) % password.length;
|
||||
}
|
||||
|
||||
initialHash.update(buffer);
|
||||
let initialKey = initialHash.digest();
|
||||
|
||||
// Localize the key with engine ID
|
||||
let localHash;
|
||||
if (authProtocol === 'SHA') {
|
||||
localHash = crypto.createHash('sha1');
|
||||
} else {
|
||||
localHash = crypto.createHash('md5');
|
||||
}
|
||||
|
||||
localHash.update(initialKey);
|
||||
localHash.update(engineId);
|
||||
localHash.update(initialKey);
|
||||
|
||||
return localHash.digest();
|
||||
} catch (error) {
|
||||
console.error('Error localizing auth key:', error);
|
||||
// Return a fallback key
|
||||
return Buffer.from(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a discovery message for SNMPv3 engine ID discovery
|
||||
* @param config SNMP configuration
|
||||
* @param requestID Request ID
|
||||
* @returns Discovery message
|
||||
*/
|
||||
public static createDiscoveryMessage(config: SnmpConfig, requestID: number): Buffer {
|
||||
// Basic SNMPv3 header for discovery
|
||||
const msgIdBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
||||
SnmpEncoder.encodeInteger(requestID)
|
||||
]);
|
||||
|
||||
const msgMaxSizeBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
||||
SnmpEncoder.encodeInteger(65507) // Max message size
|
||||
]);
|
||||
|
||||
const msgFlagsBuf = Buffer.concat([
|
||||
Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1
|
||||
Buffer.from([0x00]) // No authentication or privacy
|
||||
]);
|
||||
|
||||
const msgSecModelBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x03]) // Security model (3 = USM)
|
||||
]);
|
||||
|
||||
// SNMPv3 header
|
||||
const msgHeaderBuf = Buffer.concat([
|
||||
Buffer.from([0x30]), // ASN.1 Sequence
|
||||
Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length
|
||||
msgIdBuf,
|
||||
msgMaxSizeBuf,
|
||||
msgFlagsBuf,
|
||||
msgSecModelBuf
|
||||
]);
|
||||
|
||||
// Simple security parameters for discovery
|
||||
const securityBuf = Buffer.concat([
|
||||
Buffer.from([0x04, 0x00]), // Empty octet string
|
||||
]);
|
||||
|
||||
// Simple Get request for discovery
|
||||
const requestIdBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
||||
SnmpEncoder.encodeInteger(requestID + 1)
|
||||
]);
|
||||
|
||||
const errorStatusBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x00]) // Error Status (0 = no error)
|
||||
]);
|
||||
|
||||
const errorIndexBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x00]) // Error Index (0)
|
||||
]);
|
||||
|
||||
// Empty varbinds for discovery
|
||||
const varBindingsBuf = Buffer.concat([
|
||||
Buffer.from([0x30, 0x00]), // Empty sequence
|
||||
]);
|
||||
|
||||
const pduBuf = Buffer.concat([
|
||||
Buffer.from([0xa0]), // GetRequest
|
||||
Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]),
|
||||
requestIdBuf,
|
||||
errorStatusBuf,
|
||||
errorIndexBuf,
|
||||
varBindingsBuf
|
||||
]);
|
||||
|
||||
// Context data
|
||||
const contextEngineBuf = Buffer.concat([
|
||||
Buffer.from([0x04, 0x00]), // Empty octet string
|
||||
]);
|
||||
|
||||
const contextNameBuf = Buffer.concat([
|
||||
Buffer.from([0x04, 0x00]), // Empty octet string
|
||||
]);
|
||||
|
||||
const scopedPduBuf = Buffer.concat([
|
||||
Buffer.from([0x30]),
|
||||
Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]),
|
||||
contextEngineBuf,
|
||||
contextNameBuf,
|
||||
pduBuf
|
||||
]);
|
||||
|
||||
// Version
|
||||
const versionBuf = Buffer.concat([
|
||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
||||
Buffer.from([0x03]) // SNMP version 3 (3)
|
||||
]);
|
||||
|
||||
// Complete message
|
||||
return Buffer.concat([
|
||||
Buffer.from([0x30]),
|
||||
Buffer.from([versionBuf.length + msgHeaderBuf.length + securityBuf.length + scopedPduBuf.length]),
|
||||
versionBuf,
|
||||
msgHeaderBuf,
|
||||
securityBuf,
|
||||
scopedPduBuf
|
||||
]);
|
||||
}
|
||||
}
|
553
ts/snmp/packet-parser.ts
Normal file
553
ts/snmp/packet-parser.ts
Normal file
@ -0,0 +1,553 @@
|
||||
import type { SnmpConfig } from './types.js';
|
||||
import { SnmpEncoder } from './encoder.js';
|
||||
|
||||
/**
|
||||
* SNMP packet parsing utilities
|
||||
* Parses SNMP response packets
|
||||
*/
|
||||
export class SnmpPacketParser {
|
||||
/**
|
||||
* Parse an SNMP response
|
||||
* @param buffer Response buffer
|
||||
* @param config SNMP configuration
|
||||
* @param debug Whether to enable debug output
|
||||
* @returns Parsed value or null if parsing failed
|
||||
*/
|
||||
public static parseSnmpResponse(buffer: Buffer, config: SnmpConfig, debug: boolean = false): any {
|
||||
// Check if we have a response packet
|
||||
if (buffer[0] !== 0x30) {
|
||||
throw new Error('Invalid SNMP response format');
|
||||
}
|
||||
|
||||
// For SNMPv3, we need to handle the message differently
|
||||
if (config.version === 3) {
|
||||
return this.parseSnmpV3Response(buffer, debug);
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log('Parsing SNMPv1/v2 response: ', buffer.toString('hex'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Enhanced structured parsing approach
|
||||
// SEQUENCE header
|
||||
let pos = 0;
|
||||
if (buffer[pos] !== 0x30) {
|
||||
throw new Error('Missing SEQUENCE at start of response');
|
||||
}
|
||||
// Skip SEQUENCE header - assume length is in single byte for simplicity
|
||||
// In a more robust implementation, we'd handle multi-byte lengths
|
||||
pos += 2;
|
||||
|
||||
// VERSION
|
||||
if (buffer[pos] !== 0x02) {
|
||||
throw new Error('Missing INTEGER for version');
|
||||
}
|
||||
const versionLength = buffer[pos + 1];
|
||||
pos += 2 + versionLength;
|
||||
|
||||
// COMMUNITY STRING
|
||||
if (buffer[pos] !== 0x04) {
|
||||
throw new Error('Missing OCTET STRING for community');
|
||||
}
|
||||
const communityLength = buffer[pos + 1];
|
||||
pos += 2 + communityLength;
|
||||
|
||||
// PDU TYPE - should be RESPONSE (0xA2)
|
||||
if (buffer[pos] !== 0xA2) {
|
||||
throw new Error(`Unexpected PDU type: 0x${buffer[pos].toString(16)}, expected 0xA2`);
|
||||
}
|
||||
// Skip PDU header
|
||||
pos += 2;
|
||||
|
||||
// REQUEST ID
|
||||
if (buffer[pos] !== 0x02) {
|
||||
throw new Error('Missing INTEGER for request ID');
|
||||
}
|
||||
const requestIdLength = buffer[pos + 1];
|
||||
pos += 2 + requestIdLength;
|
||||
|
||||
// ERROR STATUS
|
||||
if (buffer[pos] !== 0x02) {
|
||||
throw new Error('Missing INTEGER for error status');
|
||||
}
|
||||
const errorStatusLength = buffer[pos + 1];
|
||||
const errorStatus = SnmpEncoder.decodeInteger(buffer, pos + 2, errorStatusLength);
|
||||
|
||||
if (errorStatus !== 0) {
|
||||
throw new Error(`SNMP error status: ${errorStatus}`);
|
||||
}
|
||||
pos += 2 + errorStatusLength;
|
||||
|
||||
// ERROR INDEX
|
||||
if (buffer[pos] !== 0x02) {
|
||||
throw new Error('Missing INTEGER for error index');
|
||||
}
|
||||
const errorIndexLength = buffer[pos + 1];
|
||||
pos += 2 + errorIndexLength;
|
||||
|
||||
// VARBIND LIST
|
||||
if (buffer[pos] !== 0x30) {
|
||||
throw new Error('Missing SEQUENCE for varbind list');
|
||||
}
|
||||
// Skip varbind list header
|
||||
pos += 2;
|
||||
|
||||
// VARBIND
|
||||
if (buffer[pos] !== 0x30) {
|
||||
throw new Error('Missing SEQUENCE for varbind');
|
||||
}
|
||||
// Skip varbind header
|
||||
pos += 2;
|
||||
|
||||
// OID
|
||||
if (buffer[pos] !== 0x06) {
|
||||
throw new Error('Missing OBJECT IDENTIFIER for OID');
|
||||
}
|
||||
const oidLength = buffer[pos + 1];
|
||||
pos += 2 + oidLength;
|
||||
|
||||
// VALUE - this is what we want
|
||||
const valueType = buffer[pos];
|
||||
const valueLength = buffer[pos + 1];
|
||||
|
||||
if (debug) {
|
||||
console.log(`Found value type: 0x${valueType.toString(16)}, length: ${valueLength}`);
|
||||
}
|
||||
|
||||
return this.parseValueByType(valueType, valueLength, buffer, pos, debug);
|
||||
} catch (error) {
|
||||
if (debug) {
|
||||
console.error('Error in structured parsing:', error);
|
||||
console.error('Falling back to scan-based parsing method');
|
||||
}
|
||||
|
||||
return this.scanBasedParsing(buffer, debug);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse value by ASN.1 type
|
||||
* @param valueType ASN.1 type
|
||||
* @param valueLength Value length
|
||||
* @param buffer Buffer containing the value
|
||||
* @param pos Position of the value in the buffer
|
||||
* @param debug Whether to enable debug output
|
||||
* @returns Parsed value
|
||||
*/
|
||||
private static parseValueByType(
|
||||
valueType: number,
|
||||
valueLength: number,
|
||||
buffer: Buffer,
|
||||
pos: number,
|
||||
debug: boolean
|
||||
): any {
|
||||
switch (valueType) {
|
||||
case 0x02: // INTEGER
|
||||
{
|
||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
||||
if (debug) {
|
||||
console.log('Parsed INTEGER value:', value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
case 0x04: // OCTET STRING
|
||||
{
|
||||
const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString();
|
||||
if (debug) {
|
||||
console.log('Parsed OCTET STRING value:', value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
case 0x05: // NULL
|
||||
if (debug) {
|
||||
console.log('Parsed NULL value');
|
||||
}
|
||||
return null;
|
||||
|
||||
case 0x06: // OBJECT IDENTIFIER (rare in a value position)
|
||||
{
|
||||
// Usually this would be encoded as a string representation
|
||||
const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString('hex');
|
||||
if (debug) {
|
||||
console.log('Parsed OBJECT IDENTIFIER value (hex):', value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
case 0x40: // IP ADDRESS
|
||||
{
|
||||
if (valueLength !== 4) {
|
||||
throw new Error(`Invalid IP address length: ${valueLength}, expected 4`);
|
||||
}
|
||||
const octets = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
octets.push(buffer[pos + 2 + i]);
|
||||
}
|
||||
const value = octets.join('.');
|
||||
if (debug) {
|
||||
console.log('Parsed IP ADDRESS value:', value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
case 0x41: // COUNTER
|
||||
case 0x42: // GAUGE32
|
||||
case 0x43: // TIMETICKS
|
||||
case 0x44: // OPAQUE
|
||||
{
|
||||
// All these are essentially unsigned 32-bit integers
|
||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
||||
if (debug) {
|
||||
console.log(`Parsed ${valueType === 0x41 ? 'COUNTER'
|
||||
: valueType === 0x42 ? 'GAUGE32'
|
||||
: valueType === 0x43 ? 'TIMETICKS'
|
||||
: 'OPAQUE'} value:`, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
default:
|
||||
if (debug) {
|
||||
console.log(`Unknown value type: 0x${valueType.toString(16)}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback scan-based parsing method
|
||||
* @param buffer Buffer containing the SNMP response
|
||||
* @param debug Whether to enable debug output
|
||||
* @returns Parsed value or null if parsing failed
|
||||
*/
|
||||
private static scanBasedParsing(buffer: Buffer, debug: boolean): any {
|
||||
// Look for various data types in the response
|
||||
// The value is near the end of the packet after the OID
|
||||
|
||||
// We're looking for one of these:
|
||||
// 0x02 - Integer - can be at the end of a varbind
|
||||
// 0x04 - OctetString
|
||||
// 0x05 - Null
|
||||
// 0x42 - Gauge32 - special type for unsigned 32-bit integers
|
||||
// 0x43 - Timeticks - special type for time values
|
||||
|
||||
// This algorithm performs a thorough search for data types
|
||||
// by iterating from the start and watching for varbind structures
|
||||
|
||||
// Walk through the buffer looking for varbinds
|
||||
let i = 0;
|
||||
|
||||
// First, find the varbinds section (0x30 sequence)
|
||||
while (i < buffer.length - 2) {
|
||||
// Look for a varbinds sequence
|
||||
if (buffer[i] === 0x30) {
|
||||
const varbindsLength = buffer[i + 1];
|
||||
const varbindsEnd = i + 2 + varbindsLength;
|
||||
|
||||
// Now search within the varbinds for the value
|
||||
let j = i + 2;
|
||||
while (j < varbindsEnd - 2) {
|
||||
// Look for a varbind (0x30 sequence)
|
||||
if (buffer[j] === 0x30) {
|
||||
const varbindLength = buffer[j + 1];
|
||||
const varbindEnd = j + 2 + varbindLength;
|
||||
|
||||
// Skip over the OID and find the value within this varbind
|
||||
let k = j + 2;
|
||||
while (k < varbindEnd - 1) {
|
||||
// First find the OID
|
||||
if (buffer[k] === 0x06) { // OID
|
||||
const oidLength = buffer[k + 1];
|
||||
k += 2 + oidLength; // Skip past the OID
|
||||
|
||||
// We should now be at the value
|
||||
// Check what type it is
|
||||
if (k < varbindEnd - 1) {
|
||||
return this.parseValueAtPosition(buffer, k, debug);
|
||||
}
|
||||
|
||||
// If we didn't find a value, move to next byte
|
||||
k++;
|
||||
} else {
|
||||
// Move to next byte
|
||||
k++;
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next varbind
|
||||
j = varbindEnd;
|
||||
} else {
|
||||
// Move to next byte
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next sequence
|
||||
i = varbindsEnd;
|
||||
} else {
|
||||
// Move to next byte
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log('No valid value found in SNMP response');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse value at a specific position in the buffer
|
||||
* @param buffer Buffer containing the SNMP response
|
||||
* @param pos Position of the value in the buffer
|
||||
* @param debug Whether to enable debug output
|
||||
* @returns Parsed value or null if parsing failed
|
||||
*/
|
||||
private static parseValueAtPosition(buffer: Buffer, pos: number, debug: boolean): any {
|
||||
if (buffer[pos] === 0x02) { // Integer
|
||||
const valueLength = buffer[pos + 1];
|
||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
||||
if (debug) {
|
||||
console.log('Found Integer value:', value);
|
||||
}
|
||||
return value;
|
||||
} else if (buffer[pos] === 0x42) { // Gauge32
|
||||
const valueLength = buffer[pos + 1];
|
||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
||||
if (debug) {
|
||||
console.log('Found Gauge32 value:', value);
|
||||
}
|
||||
return value;
|
||||
} else if (buffer[pos] === 0x43) { // TimeTicks
|
||||
const valueLength = buffer[pos + 1];
|
||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
||||
if (debug) {
|
||||
console.log('Found Timeticks value:', value);
|
||||
}
|
||||
return value;
|
||||
} else if (buffer[pos] === 0x04) { // OctetString
|
||||
const valueLength = buffer[pos + 1];
|
||||
if (debug) {
|
||||
console.log('Found OctetString value');
|
||||
}
|
||||
// Just return the string value as-is
|
||||
return buffer.slice(pos + 2, pos + 2 + valueLength).toString();
|
||||
} else if (buffer[pos] === 0x05) { // Null
|
||||
if (debug) {
|
||||
console.log('Found Null value');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an SNMPv3 response
|
||||
* @param buffer Buffer containing the SNMP response
|
||||
* @param debug Whether to enable debug output
|
||||
* @returns Parsed value or null if parsing failed
|
||||
*/
|
||||
public static parseSnmpV3Response(buffer: Buffer, debug: boolean = false): any {
|
||||
// SNMPv3 parsing is complex. In a real implementation, we would:
|
||||
// 1. Parse the header and get the security parameters
|
||||
// 2. Verify authentication if used
|
||||
// 3. Decrypt the PDU if privacy was used
|
||||
// 4. Extract the PDU and parse it
|
||||
|
||||
if (debug) {
|
||||
console.log('Parsing SNMPv3 response: ', buffer.toString('hex'));
|
||||
}
|
||||
|
||||
// Find the scopedPDU - it should be the last OCTET STRING in the message
|
||||
let scopedPduPos = -1;
|
||||
for (let i = buffer.length - 50; i >= 0; i--) {
|
||||
if (buffer[i] === 0x04 && buffer[i + 1] > 10) { // OCTET STRING with reasonable length
|
||||
scopedPduPos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (scopedPduPos === -1) {
|
||||
if (debug) {
|
||||
console.log('Could not find scoped PDU in SNMPv3 response');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip to the PDU content
|
||||
let pduContent = buffer.slice(scopedPduPos + 2); // Skip OCTET STRING header
|
||||
|
||||
// This improved algorithm performs a more thorough search for varbinds
|
||||
// in the scoped PDU
|
||||
|
||||
// First, look for the response PDU (sequence with tag 0xa2)
|
||||
let responsePdu = null;
|
||||
for (let i = 0; i < pduContent.length - 3; i++) {
|
||||
if (pduContent[i] === 0xa2) {
|
||||
// Found the response PDU
|
||||
const pduLength = pduContent[i + 1];
|
||||
responsePdu = pduContent.slice(i, i + 2 + pduLength);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!responsePdu) {
|
||||
// Try to find the varbinds directly
|
||||
for (let i = 0; i < pduContent.length - 3; i++) {
|
||||
if (pduContent[i] === 0x30) {
|
||||
const seqLength = pduContent[i + 1];
|
||||
if (i + 2 + seqLength <= pduContent.length) {
|
||||
// Check if this sequence might be the varbinds
|
||||
const possibleVarbinds = pduContent.slice(i, i + 2 + seqLength);
|
||||
|
||||
// Look for varbind structure inside
|
||||
for (let j = 0; j < possibleVarbinds.length - 3; j++) {
|
||||
if (possibleVarbinds[j] === 0x30) {
|
||||
// Might be a varbind - look for an OID inside
|
||||
for (let k = j; k < j + 10 && k < possibleVarbinds.length - 1; k++) {
|
||||
if (possibleVarbinds[k] === 0x06) {
|
||||
// Found an OID, so this is likely the varbinds sequence
|
||||
responsePdu = possibleVarbinds;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (responsePdu) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (responsePdu) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!responsePdu) {
|
||||
if (debug) {
|
||||
console.log('Could not find response PDU in SNMPv3 response');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now that we have the response PDU, search for varbinds
|
||||
// Skip the first few bytes to get past the header fields
|
||||
let varbindsPos = -1;
|
||||
for (let i = 10; i < responsePdu.length - 3; i++) {
|
||||
if (responsePdu[i] === 0x30) {
|
||||
// Check if this is the start of the varbinds
|
||||
// by seeing if it contains a varbind sequence
|
||||
for (let j = i + 2; j < i + 10 && j < responsePdu.length - 3; j++) {
|
||||
if (responsePdu[j] === 0x30) {
|
||||
varbindsPos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (varbindsPos !== -1) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (varbindsPos === -1) {
|
||||
if (debug) {
|
||||
console.log('Could not find varbinds in SNMPv3 response');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the varbinds
|
||||
const varbindsLength = responsePdu[varbindsPos + 1];
|
||||
const varbinds = responsePdu.slice(varbindsPos, varbindsPos + 2 + varbindsLength);
|
||||
|
||||
// Now search for values inside the varbinds
|
||||
for (let i = 2; i < varbinds.length - 3; i++) {
|
||||
// Look for a varbind sequence
|
||||
if (varbinds[i] === 0x30) {
|
||||
const varbindLength = varbinds[i + 1];
|
||||
const varbind = varbinds.slice(i, i + 2 + varbindLength);
|
||||
|
||||
// Inside the varbind, look for the OID and then the value
|
||||
for (let j = 0; j < varbind.length - 3; j++) {
|
||||
if (varbind[j] === 0x06) { // OID
|
||||
const oidLength = varbind[j + 1];
|
||||
|
||||
// The value should be right after the OID
|
||||
const valuePos = j + 2 + oidLength;
|
||||
if (valuePos < varbind.length - 1) {
|
||||
// Check what type of value it is
|
||||
if (varbind[valuePos] === 0x02) { // INTEGER
|
||||
const valueLength = varbind[valuePos + 1];
|
||||
const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
|
||||
if (debug) {
|
||||
console.log('Found INTEGER value in SNMPv3 response:', value);
|
||||
}
|
||||
return value;
|
||||
} else if (varbind[valuePos] === 0x42) { // Gauge32
|
||||
const valueLength = varbind[valuePos + 1];
|
||||
const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
|
||||
if (debug) {
|
||||
console.log('Found Gauge32 value in SNMPv3 response:', value);
|
||||
}
|
||||
return value;
|
||||
} else if (varbind[valuePos] === 0x43) { // TimeTicks
|
||||
const valueLength = varbind[valuePos + 1];
|
||||
const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
|
||||
if (debug) {
|
||||
console.log('Found TimeTicks value in SNMPv3 response:', value);
|
||||
}
|
||||
return value;
|
||||
} else if (varbind[valuePos] === 0x04) { // OctetString
|
||||
const valueLength = varbind[valuePos + 1];
|
||||
const value = varbind.slice(valuePos + 2, valuePos + 2 + valueLength).toString();
|
||||
if (debug) {
|
||||
console.log('Found OctetString value in SNMPv3 response:', value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log('No valid value found in SNMPv3 response');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract engine ID from SNMPv3 response
|
||||
* @param buffer Buffer containing the SNMP response
|
||||
* @param debug Whether to enable debug output
|
||||
* @returns Extracted engine ID or null if extraction failed
|
||||
*/
|
||||
public static extractEngineId(buffer: Buffer, debug: boolean = false): Buffer | null {
|
||||
try {
|
||||
// Simple parsing to find the engine ID
|
||||
// Look for the first octet string with appropriate length
|
||||
for (let i = 0; i < buffer.length - 10; i++) {
|
||||
if (buffer[i] === 0x04) { // Octet string
|
||||
const len = buffer[i + 1];
|
||||
if (len >= 5 && len <= 32) { // Engine IDs are typically 5-32 bytes
|
||||
// Verify this looks like an engine ID (usually starts with 0x80)
|
||||
if (buffer[i + 2] === 0x80) {
|
||||
if (debug) {
|
||||
console.log('Found engine ID at position', i);
|
||||
console.log('Engine ID:', buffer.slice(i + 2, i + 2 + len).toString('hex'));
|
||||
}
|
||||
return buffer.slice(i + 2, i + 2 + len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error extracting engine ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
90
ts/snmp/types.ts
Normal file
90
ts/snmp/types.ts
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Type definitions for SNMP module
|
||||
*/
|
||||
|
||||
/**
|
||||
* UPS status interface
|
||||
*/
|
||||
export interface UpsStatus {
|
||||
/** Current power status */
|
||||
powerStatus: 'online' | 'onBattery' | 'unknown';
|
||||
/** Battery capacity percentage */
|
||||
batteryCapacity: number;
|
||||
/** Remaining runtime in minutes */
|
||||
batteryRuntime: number;
|
||||
/** Raw values from SNMP responses */
|
||||
raw: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SNMP OID Sets for different UPS brands
|
||||
*/
|
||||
export interface OIDSet {
|
||||
/** OID for power status */
|
||||
POWER_STATUS: string;
|
||||
/** OID for battery capacity */
|
||||
BATTERY_CAPACITY: string;
|
||||
/** OID for battery runtime */
|
||||
BATTERY_RUNTIME: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported UPS model types
|
||||
*/
|
||||
export type UpsModel = 'cyberpower' | 'apc' | 'eaton' | 'tripplite' | 'liebert' | 'custom';
|
||||
|
||||
/**
|
||||
* SNMP Configuration interface
|
||||
*/
|
||||
export interface SnmpConfig {
|
||||
/** SNMP server host */
|
||||
host: string;
|
||||
/** SNMP server port (default 161) */
|
||||
port: number;
|
||||
/** SNMP version (1, 2, or 3) */
|
||||
version: number;
|
||||
/** Timeout in milliseconds */
|
||||
timeout: number;
|
||||
|
||||
// SNMPv1/v2c
|
||||
/** Community string for SNMPv1/v2c */
|
||||
community?: string;
|
||||
|
||||
// SNMPv3
|
||||
/** Security level for SNMPv3 */
|
||||
securityLevel?: 'noAuthNoPriv' | 'authNoPriv' | 'authPriv';
|
||||
/** Username for SNMPv3 authentication */
|
||||
username?: string;
|
||||
/** Authentication protocol for SNMPv3 */
|
||||
authProtocol?: 'MD5' | 'SHA';
|
||||
/** Authentication key for SNMPv3 */
|
||||
authKey?: string;
|
||||
/** Privacy protocol for SNMPv3 */
|
||||
privProtocol?: 'DES' | 'AES';
|
||||
/** Privacy key for SNMPv3 */
|
||||
privKey?: string;
|
||||
|
||||
// UPS model and custom OIDs
|
||||
/** UPS model for OID selection */
|
||||
upsModel?: UpsModel;
|
||||
/** Custom OIDs when using custom UPS model */
|
||||
customOIDs?: OIDSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* SNMPv3 security parameters
|
||||
*/
|
||||
export interface SnmpV3SecurityParams {
|
||||
/** Engine ID for the SNMP server */
|
||||
msgAuthoritativeEngineID: Buffer;
|
||||
/** Engine boots counter */
|
||||
msgAuthoritativeEngineBoots: number;
|
||||
/** Engine time counter */
|
||||
msgAuthoritativeEngineTime: number;
|
||||
/** Username for authentication */
|
||||
msgUserName: string;
|
||||
/** Authentication parameters */
|
||||
msgAuthenticationParameters: Buffer;
|
||||
/** Privacy parameters */
|
||||
msgPrivacyParameters: Buffer;
|
||||
}
|
246
ts/systemd.ts
Normal file
246
ts/systemd.ts
Normal file
@ -0,0 +1,246 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { NupstDaemon } from './daemon.js';
|
||||
|
||||
/**
|
||||
* Class for managing systemd service
|
||||
* Handles installation, removal, and control of the NUPST systemd service
|
||||
*/
|
||||
export class NupstSystemd {
|
||||
/** Path to the systemd service file */
|
||||
private readonly serviceFilePath = '/etc/systemd/system/nupst.service';
|
||||
private readonly daemon: NupstDaemon;
|
||||
|
||||
/** Template for the systemd service file */
|
||||
private readonly serviceTemplate = `[Unit]
|
||||
Description=Node.js UPS Shutdown Tool
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/nupst daemon-start
|
||||
Restart=always
|
||||
User=root
|
||||
Group=root
|
||||
Environment=PATH=/usr/bin:/usr/local/bin
|
||||
Environment=NODE_ENV=production
|
||||
WorkingDirectory=/tmp
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`;
|
||||
|
||||
/**
|
||||
* Create a new systemd manager
|
||||
* @param daemon The daemon instance to manage
|
||||
*/
|
||||
constructor(daemon: NupstDaemon) {
|
||||
this.daemon = daemon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a configuration file exists
|
||||
* @private
|
||||
* @throws Error if configuration not found
|
||||
*/
|
||||
private async checkConfigExists(): Promise<void> {
|
||||
const configPath = '/etc/nupst/config.json';
|
||||
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('└──────────────────────────────────────────┘');
|
||||
throw new Error('Configuration not found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the systemd service file
|
||||
* @throws Error if installation fails
|
||||
*/
|
||||
public async install(): Promise<void> {
|
||||
try {
|
||||
// Check if configuration exists before installing
|
||||
await this.checkConfigExists();
|
||||
|
||||
// Write the service file
|
||||
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
|
||||
console.log('┌─ Service Installation ─────────────────────┐');
|
||||
console.log(`│ Service file created at ${this.serviceFilePath}`);
|
||||
|
||||
// Reload systemd daemon
|
||||
execSync('systemctl daemon-reload');
|
||||
console.log('│ Systemd daemon reloaded');
|
||||
|
||||
// Enable the service
|
||||
execSync('systemctl enable nupst.service');
|
||||
console.log('│ Service enabled to start on boot');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
} 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);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the systemd service
|
||||
* @throws Error if start fails
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
try {
|
||||
// Check if configuration exists before starting
|
||||
await this.checkConfigExists();
|
||||
|
||||
execSync('systemctl start nupst.service');
|
||||
console.log('┌─ Service Status ─────────────────────────┐');
|
||||
console.log('│ NUPST service started successfully');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
} 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);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the systemd service
|
||||
* @throws Error if stop fails
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
try {
|
||||
execSync('systemctl stop nupst.service');
|
||||
console.log('NUPST service stopped');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop service:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of the systemd service and UPS
|
||||
*/
|
||||
public async getStatus(): Promise<void> {
|
||||
try {
|
||||
// Check if config exists first
|
||||
try {
|
||||
await this.checkConfigExists();
|
||||
} catch (error) {
|
||||
// Error message already displayed by checkConfigExists
|
||||
return;
|
||||
}
|
||||
|
||||
await this.displayServiceStatus();
|
||||
await this.displayUpsStatus();
|
||||
} catch (error) {
|
||||
console.error(`Failed to get status: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the systemd service status
|
||||
* @private
|
||||
*/
|
||||
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('└──────────────────────────────────────────┘');
|
||||
} catch (error) {
|
||||
console.error('┌─ Service Status ─────────────────────────┐');
|
||||
console.error('│ Service is not running');
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the UPS status
|
||||
* @private
|
||||
*/
|
||||
private async displayUpsStatus(): Promise<void> {
|
||||
try {
|
||||
const upsStatus = await this.daemon.getConfig().snmp;
|
||||
const snmp = this.daemon.getNupstSnmp();
|
||||
const status = await snmp.getUpsStatus(upsStatus);
|
||||
|
||||
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('└──────────────────────────────────────────┘');
|
||||
} catch (error) {
|
||||
console.error('┌─ UPS Status ───────────────────────────────┐');
|
||||
console.error(`│ Failed to retrieve UPS status: ${error.message}`);
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable and uninstall the systemd service
|
||||
* @throws Error if disabling fails
|
||||
*/
|
||||
public async disable(): Promise<void> {
|
||||
try {
|
||||
await this.stopService();
|
||||
await this.disableService();
|
||||
await this.removeServiceFile();
|
||||
|
||||
// Reload systemd daemon
|
||||
execSync('systemctl daemon-reload');
|
||||
console.log('Systemd daemon reloaded');
|
||||
console.log('NUPST service has been successfully uninstalled');
|
||||
} catch (error) {
|
||||
console.error('Failed to disable and uninstall service:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the service if it's running
|
||||
* @private
|
||||
*/
|
||||
private async stopService(): Promise<void> {
|
||||
try {
|
||||
console.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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the service
|
||||
* @private
|
||||
*/
|
||||
private async disableService(): Promise<void> {
|
||||
try {
|
||||
console.log('Disabling NUPST service...');
|
||||
execSync('systemctl disable nupst.service');
|
||||
} catch (error) {
|
||||
console.log('Service was not enabled or could not be disabled');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the service file if it exists
|
||||
* @private
|
||||
*/
|
||||
private async removeServiceFile(): Promise<void> {
|
||||
if (await fs.stat(this.serviceFilePath).catch(() => null)) {
|
||||
console.log(`Removing service file ${this.serviceFilePath}...`);
|
||||
await fs.unlink(this.serviceFilePath);
|
||||
console.log('Service file removed');
|
||||
} else {
|
||||
console.log('Service file did not exist');
|
||||
}
|
||||
}
|
||||
}
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
68
uninstall.sh
Normal file
68
uninstall.sh
Normal file
@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NUPST Uninstaller Script
|
||||
# Completely removes NUPST from the system
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root (sudo ./uninstall.sh)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "NUPST Uninstaller"
|
||||
echo "================="
|
||||
echo "This script will completely remove NUPST from your system."
|
||||
|
||||
# Find the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
# Step 1: Stop and disable the systemd service if it exists
|
||||
if [ -f "/etc/systemd/system/nupst.service" ]; then
|
||||
echo "Stopping NUPST service..."
|
||||
systemctl stop nupst.service 2>/dev/null
|
||||
|
||||
echo "Disabling NUPST service..."
|
||||
systemctl disable nupst.service 2>/dev/null
|
||||
|
||||
echo "Removing systemd service file..."
|
||||
rm -f /etc/systemd/system/nupst.service
|
||||
|
||||
echo "Reloading systemd daemon..."
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
|
||||
# Step 2: Remove global symlink
|
||||
if [ -L "/usr/local/bin/nupst" ]; then
|
||||
echo "Removing global symlink..."
|
||||
rm -f /usr/local/bin/nupst
|
||||
fi
|
||||
|
||||
# Step 3: Ask about removing configuration
|
||||
read -p "Do you want to remove the NUPST configuration files? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Removing configuration files..."
|
||||
rm -rf /etc/nupst
|
||||
fi
|
||||
|
||||
# Step 4: Check if this was a git installation
|
||||
if [ -d "$SCRIPT_DIR/.git" ]; then
|
||||
echo
|
||||
echo "This appears to be a git installation. The local repository will remain intact."
|
||||
echo "If you wish to completely remove it, you can delete the directory:"
|
||||
echo " rm -rf $SCRIPT_DIR"
|
||||
fi
|
||||
|
||||
# Check for npm global installation
|
||||
NODE_PATH=$(which node 2>/dev/null)
|
||||
if [ -n "$NODE_PATH" ]; then
|
||||
NPM_PATH=$(dirname "$NODE_PATH")/npm
|
||||
if [ -x "$NPM_PATH" ]; then
|
||||
echo
|
||||
echo "If you installed NUPST via npm, you may want to uninstall it with:"
|
||||
echo " npm uninstall -g @serve.zone/nupst"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "NUPST has been uninstalled from your system."
|
Loading…
x
Reference in New Issue
Block a user