Compare commits
46 Commits
Author | SHA1 | Date | |
---|---|---|---|
00afa317ef | |||
45ee8208b5 | |||
39bf3e2239 | |||
f3de3f0618 | |||
03056d279d | |||
f860f39e59 | |||
fa4516de3b | |||
539547beb8 | |||
6eb92959ec | |||
4af9af0845 | |||
f7e12cdcbb | |||
002498b91b | |||
459911fe5f | |||
9859a02ea2 | |||
65444b6d25 | |||
d049e8741f | |||
1123a99aea | |||
d01e878310 | |||
588aeabf4b | |||
87005e72f1 | |||
f799c2ee66 | |||
1a029ba493 | |||
5b756dd223 | |||
4cac599a58 | |||
be6a7314c3 | |||
83ba9c2611 | |||
22ab472e58 | |||
9a77030377 | |||
ceff285ff5 | |||
d8bfbf0be3 | |||
3e6b883b38 | |||
47ef918128 | |||
5951638967 | |||
b06e2b2273 | |||
cc1cfe894c | |||
da49b7a5bf | |||
4de6081a74 | |||
5a13e49803 | |||
2737fca294 | |||
896233914f | |||
5bb775b17d | |||
ae8219acf7 | |||
4ad383884c | |||
65a9d1c798 | |||
f583e1466f | |||
9d893a97b6 |
55
bin/nupst
55
bin/nupst
@ -22,16 +22,63 @@ fi
|
||||
# For debugging
|
||||
# echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
# Set Node.js binary path directly
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-linux-x64/bin/node"
|
||||
# Detect architecture and OS
|
||||
ARCH=$(uname -m)
|
||||
OS=$(uname -s)
|
||||
|
||||
# Determine Node.js binary location based on architecture and OS
|
||||
NODE_BIN=""
|
||||
case "$OS" in
|
||||
Linux)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-linux-x64/bin/node"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-linux-arm64/bin/node"
|
||||
;;
|
||||
*)
|
||||
# Use system Node as fallback for other architectures
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_BIN="node"
|
||||
echo "Using system Node.js installation for unsupported architecture: $ARCH"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
Darwin)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-darwin-x64/bin/node"
|
||||
;;
|
||||
arm64)
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-darwin-arm64/bin/node"
|
||||
;;
|
||||
*)
|
||||
# Use system Node as fallback for other architectures
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_BIN="node"
|
||||
echo "Using system Node.js installation for unsupported architecture: $ARCH"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
# Use system Node as fallback for other operating systems
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_BIN="node"
|
||||
echo "Using system Node.js installation for unsupported OS: $OS"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# If binary doesn't exist, try system Node as fallback
|
||||
if [ ! -f "$NODE_BIN" ]; then
|
||||
if [ -z "$NODE_BIN" ] || [ ! -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 "Error: Node.js binary not found for $OS-$ARCH"
|
||||
echo "Please run the setup script or install Node.js manually."
|
||||
exit 1
|
||||
fi
|
||||
|
147
changelog.md
147
changelog.md
@ -1,5 +1,152 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-03-26 - 2.6.16 - fix(cli)
|
||||
Improve CLI logging consistency by replacing direct console output with unified logger calls.
|
||||
|
||||
- Replaced console.log and console.error with logger.log and logger.error in CLI commands
|
||||
- Standardized debug, error, and status messages using logger's logbox utilities
|
||||
- Enhanced consistency of log output throughout the ts/cli.ts file
|
||||
|
||||
## 2025-03-26 - 2.6.15 - fix(logger)
|
||||
Replace direct console logging with unified logger interface for consistent formatting
|
||||
|
||||
- Substitute console.log, console.error, and related calls with logger methods in cli, daemon, systemd, nupst, and index modules
|
||||
- Integrate logBox formatting for structured output and consistent log presentation
|
||||
- Update test expectations in test.logger.ts to check for standardized error messages
|
||||
- Refactor logging calls throughout the codebase for improved clarity and maintainability
|
||||
|
||||
## 2025-03-26 - 2.6.14 - fix(systemd)
|
||||
Shorten closing log divider in systemd service installation output for consistent formatting.
|
||||
|
||||
- Replaced the overly long footer with a shorter one in ts/systemd.ts.
|
||||
- This change improves log readability without affecting functionality.
|
||||
|
||||
## 2025-03-26 - 2.6.13 - fix(cli)
|
||||
Fix CLI update output box formatting
|
||||
|
||||
- Adjusted the closing box line in the update process log messages for consistent visual formatting
|
||||
|
||||
## 2025-03-26 - 2.6.12 - fix(systemd)
|
||||
Adjust logging border in systemd service installation output
|
||||
|
||||
- Updated the closing border line for consistent output formatting in ts/systemd.ts
|
||||
|
||||
## 2025-03-26 - 2.6.11 - fix(cli, systemd)
|
||||
Adjust log formatting for consistent output in CLI and systemd commands
|
||||
|
||||
- Fixed spacing issues in service installation and status log messages in the systemd module.
|
||||
- Revised output formatting in the CLI to improve message clarity.
|
||||
|
||||
## 2025-03-26 - 2.6.10 - fix(daemon)
|
||||
Adjust console log box formatting for consistent output in daemon status messages
|
||||
|
||||
- Updated closing box borders to align properly in configuration error, periodic updates, and UPS status logs
|
||||
- Improved visual consistency in log messages
|
||||
|
||||
## 2025-03-26 - 2.6.9 - fix(cli)
|
||||
Improve console output formatting for status banners and logging messages
|
||||
|
||||
- Standardize banner messages in daemon status updates
|
||||
- Refine version information banner in nupst logging
|
||||
- Update UPS connection and status banners in systemd
|
||||
|
||||
## 2025-03-26 - 2.6.8 - fix(cli)
|
||||
Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP manager
|
||||
|
||||
- Standardize whitespace and formatting in ts/cli.ts for consistency
|
||||
- Refine argument filtering for debug mode and prompt messages
|
||||
- Remove unused 'dgram' import from ts/snmp/manager.ts
|
||||
|
||||
## 2025-03-26 - 2.6.7 - fix(setup.sh)
|
||||
Clarify net-snmp dependency installation message in setup.sh
|
||||
|
||||
- Updated echo statement to indicate installation of net-snmp along with 2 subdependencies
|
||||
- Improves clarity on dependency installation during setup
|
||||
|
||||
## 2025-03-26 - 2.6.6 - fix(setup.sh)
|
||||
Improve setup script to detect and execute npm-cli.js directly using the Node.js binary
|
||||
|
||||
- Replace use of the npm binary with direct execution of npm-cli.js
|
||||
- Add fallback logic to locate npm-cli.js when not found at the expected path
|
||||
- Simplify cleanup by removing unnecessary PATH modifications
|
||||
|
||||
## 2025-03-26 - 2.6.5 - fix(daemon, setup)
|
||||
Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm paths
|
||||
|
||||
- Use execFileAsync to execute shutdown commands reliably
|
||||
- Add multiple fallback alternatives for shutdown and emergency shutdown handling
|
||||
- Update setup.sh to log the Node and NPM versions using absolute paths without modifying PATH
|
||||
|
||||
## 2025-03-26 - 2.6.4 - fix(setup)
|
||||
Improve installation process in setup script by cleaning up package files and ensuring a minimal net-snmp dependency installation.
|
||||
|
||||
- Remove existing package-lock.json along with node_modules to prevent stale artifacts.
|
||||
- Back up the original package.json before modifying it.
|
||||
- Create a minimal package.json with only the net-snmp dependency based on the backed-up version.
|
||||
- Use a clean install to guarantee that only net-snmp is installed.
|
||||
- Restore the original package.json if the installation fails.
|
||||
|
||||
## 2025-03-26 - 2.6.3 - fix(setup)
|
||||
Update setup script to install only net-snmp dependency and create a minimal package-lock.json for better dependency control.
|
||||
|
||||
- Removed full production dependency install in favor of installing only net-snmp@3.20.0
|
||||
- Added verification step to confirm net-snmp installation
|
||||
- Generate a minimal package-lock.json if one does not exist
|
||||
|
||||
## 2025-03-26 - 2.6.2 - fix(setup/readme)
|
||||
Improve force update instructions and dependency installation process in setup.sh and readme.md
|
||||
|
||||
- Clarify force update commands with explicit paths in readme.md
|
||||
- Remove existing node_modules before installing dependencies in setup.sh
|
||||
- Switch from 'npm ci --only=production' to 'npm install --omit=dev' with updated error instructions
|
||||
|
||||
## 2025-03-26 - 2.6.1 - fix(setup)
|
||||
Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log Node and npm versions, and restore the original PATH afterwards.
|
||||
|
||||
- Temporarily prepend vendor Node.js binary directory to PATH to ensure proper npm execution.
|
||||
- Log Node.js and npm versions for debugging purposes.
|
||||
- Restore the original PATH after installing dependencies.
|
||||
|
||||
## 2025-03-26 - 2.6.0 - feat(setup)
|
||||
Add --force update flag to setup script and update installation instructions
|
||||
|
||||
- Implemented --force option in setup.sh to force-update Node.js binary and dependencies
|
||||
- Updated readme.md to document the --force flag and revised update steps
|
||||
- Modified ts/cli.ts update command to pass the --force flag to setup.sh
|
||||
|
||||
## 2025-03-26 - 2.5.2 - fix(installer)
|
||||
Improve Node.js binary detection, dependency management, and SNMPv3 fallback logic
|
||||
|
||||
- Enhanced bin/nupst to detect OS and architecture (Linux and Darwin) and fall back to system Node.js for unsupported platforms
|
||||
- Moved net-snmp from devDependencies to dependencies in package.json
|
||||
- Updated setup.sh to install production dependencies and handle installation errors gracefully
|
||||
- Refined SNMPv3 user configuration and fallback mechanism in ts/snmp/manager.ts
|
||||
- Revised README to clarify minimal runtime dependencies and secure SNMP features
|
||||
|
||||
## 2025-03-25 - 2.5.1 - fix(snmp)
|
||||
Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion.
|
||||
|
||||
- Updated Eaton UPS power status OID to '1.3.6.1.4.1.534.1.4.4.0' to correctly detect online/battery status.
|
||||
- Added conversion for Eaton UPS battery runtime from seconds to minutes in SNMP manager.
|
||||
|
||||
## 2025-03-25 - 2.5.0 - feat(cli)
|
||||
Automatically restart running NUPST service after configuration changes in interactive setup
|
||||
|
||||
- Added restartServiceIfRunning() to check and restart the service if it's active.
|
||||
- Invoked the restart function post-setup to apply configuration changes immediately.
|
||||
|
||||
## 2025-03-25 - 2.4.8 - fix(installer)
|
||||
Improve Git dependency handling and repository cloning in install.sh
|
||||
|
||||
- Add explicit check for git installation and prompt the user interactively if git is missing.
|
||||
- Auto-install git when '-y' flag is provided in non-interactive mode.
|
||||
- Ensure proper cloning of the repository when running the installer outside the repo.
|
||||
|
||||
## 2025-03-25 - 2.4.7 - fix(readme)
|
||||
Update installation instructions to combine download and execution into a single command for clarity
|
||||
|
||||
- Method 1 now uses a unified one-line command to download and run the install script
|
||||
|
||||
## 2025-03-25 - 2.4.6 - fix(installer)
|
||||
Improve installation instructions for interactive and non-interactive setups
|
||||
|
||||
|
85
install.sh
85
install.sh
@ -162,37 +162,37 @@ install_git() {
|
||||
INSTALL_DIR="/opt/nupst"
|
||||
REPO_URL="https://code.foss.global/serve.zone/nupst.git"
|
||||
|
||||
if [ $PIPED -eq 1 ]; then
|
||||
echo "Installing NUPST from remote repository..."
|
||||
# Check if git is installed - needed for both piped and direct execution
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "Git is required but not installed."
|
||||
|
||||
# Check if git is installed
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "Git is required but not installed."
|
||||
if [ $AUTO_YES -eq 1 ]; then
|
||||
echo "Auto-installing git (-y flag provided)..."
|
||||
install_git
|
||||
elif [ $INTERACTIVE -eq 1 ]; then
|
||||
# If interactive and no -y flag, ask the user
|
||||
echo "Would you like to install git now? (y/N): "
|
||||
read -r install_git_prompt
|
||||
|
||||
if [ $AUTO_YES -eq 1 ]; then
|
||||
echo "Auto-installing git (-y flag provided)..."
|
||||
if [[ "$install_git_prompt" =~ ^[Yy]$ ]]; then
|
||||
install_git
|
||||
elif [ $INTERACTIVE -eq 1 ]; then
|
||||
# If interactive and no -y flag, ask the user
|
||||
echo "Would you like to install git now? (y/N): "
|
||||
read -r install_git_prompt
|
||||
|
||||
if [[ "$install_git_prompt" =~ ^[Yy]$ ]]; then
|
||||
install_git
|
||||
else
|
||||
echo "Git installation skipped. Please install git manually and run the installer again."
|
||||
echo "Alternatively, you can run the installer with -y flag to automatically install git:"
|
||||
echo " sudo bash install.sh -y"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Non-interactive mode without -y flag
|
||||
echo "Error: Git is required but not installed."
|
||||
echo "In non-interactive mode, use -y flag to auto-install dependencies:"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y"
|
||||
echo "Git installation skipped. Please install git manually and run the installer again."
|
||||
echo "Alternatively, you can run the installer with -y flag to automatically install git:"
|
||||
echo " sudo bash install.sh -y"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Non-interactive mode without -y flag
|
||||
echo "Error: Git is required but not installed."
|
||||
echo "In non-interactive mode, use -y flag to auto-install dependencies:"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $PIPED -eq 1 ]; then
|
||||
echo "Installing NUPST from remote repository..."
|
||||
|
||||
# Check if installation directory exists
|
||||
if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then
|
||||
@ -235,12 +235,47 @@ if [ $PIPED -eq 1 ]; then
|
||||
# Set script directory to the cloned repo
|
||||
SCRIPT_DIR="$INSTALL_DIR"
|
||||
else
|
||||
# Running directly from within the repo
|
||||
# Running directly from within the repo or downloaded script
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
# When running from a downloaded script in a different location
|
||||
# we need to clone the repository first
|
||||
if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then
|
||||
echo "Running installer from downloaded script outside repository."
|
||||
echo "Will clone the repository to $INSTALL_DIR..."
|
||||
|
||||
# Create installation directory if needed
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Removing previous installation at $INSTALL_DIR..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Clone the repository
|
||||
echo "Cloning NUPST repository to $INSTALL_DIR..."
|
||||
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to clone repository. Please check your internet connection."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update script directory to use the cloned repo
|
||||
SCRIPT_DIR="$INSTALL_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run setup script
|
||||
echo "Running setup script..."
|
||||
if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then
|
||||
echo "ERROR: Setup script not found at $SCRIPT_DIR/setup.sh"
|
||||
echo "Current directory: $(pwd)"
|
||||
echo "Script directory: $SCRIPT_DIR"
|
||||
ls -la "$SCRIPT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash "$SCRIPT_DIR/setup.sh"
|
||||
|
||||
# Install globally
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "2.4.6",
|
||||
"version": "2.6.16",
|
||||
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
@ -36,7 +36,9 @@
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"net-snmp": "3.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.3.2",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
@ -54,5 +56,6 @@
|
||||
"mongodb-memory-server",
|
||||
"puppeteer"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
||||
|
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -7,6 +7,10 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
net-snmp:
|
||||
specifier: 3.20.0
|
||||
version: 3.20.0
|
||||
devDependencies:
|
||||
'@git.zone/tsbuild':
|
||||
specifier: ^2.3.2
|
||||
@ -1647,6 +1651,9 @@ packages:
|
||||
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
asn1-ber@1.2.2:
|
||||
resolution: {integrity: sha512-CbNem/7hxrjSiOAOOTX4iZxu+0m3jiLqlsERQwwPM1IDR/22M8IPpA1VVndCLw5KtjRYyRODbvAEIfuTogNDng==}
|
||||
|
||||
ast-types@0.13.4:
|
||||
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
|
||||
engines: {node: '>=4'}
|
||||
@ -3303,6 +3310,9 @@ packages:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
net-snmp@3.20.0:
|
||||
resolution: {integrity: sha512-4Cp8ODkzgVXjUrIQFfL9Vo6qVsz+8OuAjUvkRGsSZOKSpoxpy9YWjVgNs+/a9N4Hd9MilIy90Zhw3EZlUUZB6A==}
|
||||
|
||||
netmask@2.0.2:
|
||||
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
@ -7181,6 +7191,8 @@ snapshots:
|
||||
|
||||
array-union@2.1.0: {}
|
||||
|
||||
asn1-ber@1.2.2: {}
|
||||
|
||||
ast-types@0.13.4:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@ -9133,6 +9145,11 @@ snapshots:
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
net-snmp@3.20.0:
|
||||
dependencies:
|
||||
asn1-ber: 1.2.2
|
||||
smart-buffer: 4.2.0
|
||||
|
||||
netmask@2.0.2: {}
|
||||
|
||||
new-find-package-json@2.0.0:
|
||||
|
40
readme.md
40
readme.md
@ -20,8 +20,7 @@ NUPST is a command-line tool that monitors SNMP-enabled UPS devices and initiate
|
||||
|
||||
```bash
|
||||
# Method 1: Download and run (most reliable across all environments)
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh
|
||||
sudo bash nupst-install.sh
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh && sudo bash nupst-install.sh && rm nupst-install.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
@ -228,8 +227,19 @@ sudo nupst update
|
||||
This will:
|
||||
1. Pull the latest changes from the git repository
|
||||
2. Run the installation scripts
|
||||
3. Refresh the systemd service configuration
|
||||
4. Restart the service if it was running
|
||||
3. Force-update Node.js and all dependencies, even if they already exist
|
||||
4. Refresh the systemd service configuration
|
||||
5. Restart the service if it was running
|
||||
|
||||
You can also manually run the setup script with the force flag to update Node.js and dependencies without updating the application code:
|
||||
|
||||
```bash
|
||||
# If you're in the nupst directory:
|
||||
bash ./setup.sh --force
|
||||
|
||||
# If you're in another directory, specify the full path:
|
||||
bash /opt/nupst/setup.sh --force
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
@ -237,10 +247,10 @@ NUPST was designed with security in mind:
|
||||
|
||||
### Minimal Dependencies
|
||||
|
||||
- **Zero Runtime NPM Dependencies**: NUPST is built without any external NPM packages to minimize the attack surface and avoid supply chain risks.
|
||||
- **Minimal Runtime Dependencies**: NUPST uses only one carefully selected NPM package (net-snmp) to minimize the attack surface and avoid supply chain risks while providing robust SNMP functionality.
|
||||
- **Self-contained Node.js**: NUPST ships with its own Node.js binary, isolated from the system's Node.js installation. This ensures:
|
||||
- No dependency on system Node.js versions
|
||||
- Zero external libraries that could become compromised
|
||||
- Minimal external libraries that could become compromised
|
||||
- Consistent, tested environment for execution
|
||||
- Reduced risk of dependency-based attacks
|
||||
|
||||
@ -248,14 +258,30 @@ NUPST was designed with security in mind:
|
||||
|
||||
- **Privilege Separation**: Only specific commands that require elevated permissions (`enable`, `disable`, `update`) check for root access; all other functionality runs with minimal privileges.
|
||||
- **Limited Network Access**: NUPST only communicates with the UPS device over SNMP and contacts npmjs.org only to check for updates.
|
||||
- **Secure SNMPv3 Support**: Supports encrypted authentication and privacy for secure communication with the UPS device.
|
||||
- **Isolated Execution**: The application runs in its working directory (`/opt/nupst`) or specified installation location, minimizing the impact on the rest of the system.
|
||||
|
||||
### SNMP Security Features
|
||||
|
||||
- **SNMPv3 Support with Secure Authentication and Privacy**:
|
||||
- Three security levels available:
|
||||
- `noAuthNoPriv`: No authentication or encryption (basic access)
|
||||
- `authNoPriv`: Authentication without encryption (verifies identity)
|
||||
- `authPriv`: Full authentication and encryption (most secure)
|
||||
- Authentication protocols: MD5 or SHA
|
||||
- Privacy/encryption protocols: DES or AES
|
||||
- Automatic fallback mechanisms for compatibility
|
||||
- Context support for segmented SNMP deployments
|
||||
- Configurable timeouts based on security level
|
||||
- **Graceful degradation**: If authentication or privacy details are missing or invalid, NUPST will automatically fall back to a lower security level while logging appropriate warnings.
|
||||
- **Interactive setup**: Guided setup process to properly configure SNMPv3 security settings with clear explanations of each security option.
|
||||
|
||||
### Installation Security
|
||||
|
||||
- The installation script can be reviewed before execution (`curl -sSL [url] | less`)
|
||||
- All setup scripts download only verified versions and check integrity
|
||||
- Installation is transparent and places files in standard locations (`/opt/nupst`, `/usr/local/bin`, `/etc/systemd/system`)
|
||||
- Automatically detects platform architecture and OS for proper binary selection
|
||||
- Installs production dependencies locally without requiring global npm packages
|
||||
|
||||
### Audit and Review
|
||||
|
||||
|
103
setup.sh
103
setup.sh
@ -2,6 +2,22 @@
|
||||
|
||||
# NUPST Setup Script
|
||||
# Downloads the appropriate Node.js binary for the current platform
|
||||
# and installs production dependencies
|
||||
|
||||
# Parse command line arguments
|
||||
FORCE_UPDATE=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--force|-f)
|
||||
FORCE_UPDATE=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
# Unknown option
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Find the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
@ -74,8 +90,9 @@ case "$OS" in
|
||||
esac
|
||||
|
||||
# Check if we already have the Node.js binary
|
||||
if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ]; then
|
||||
if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ] && [ $FORCE_UPDATE -eq 0 ]; then
|
||||
echo "Node.js binary already exists for $OS-$ARCH. Skipping download."
|
||||
echo "Use --force or -f to force update Node.js."
|
||||
else
|
||||
echo "Downloading Node.js v$NODE_VERSION for $OS-$ARCH..."
|
||||
|
||||
@ -222,6 +239,90 @@ echo "dist_ts directory successfully downloaded from npm registry."
|
||||
# Make launcher script executable
|
||||
chmod +x "$SCRIPT_DIR/bin/nupst"
|
||||
|
||||
# Set up Node.js binary path
|
||||
NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin"
|
||||
NODE_BIN="$NODE_BIN_DIR/node"
|
||||
NPM_CLI_JS="$NODE_BIN_DIR/../lib/node_modules/npm/bin/npm-cli.js"
|
||||
|
||||
# Ensure we have executable permissions
|
||||
chmod +x "$NODE_BIN"
|
||||
|
||||
# Make sure the npm-cli.js exists
|
||||
if [ ! -f "$NPM_CLI_JS" ]; then
|
||||
# Try to find npm-cli.js
|
||||
NPM_CLI_JS=$(find "$NODE_BIN_DIR/.." -name "npm-cli.js" | head -1)
|
||||
|
||||
if [ -z "$NPM_CLI_JS" ]; then
|
||||
echo "Warning: Could not find npm-cli.js, npm commands may fail"
|
||||
# Set to a fallback value so code can continue
|
||||
NPM_CLI_JS="$NODE_BIN_DIR/npm"
|
||||
else
|
||||
echo "Found npm-cli.js at: $NPM_CLI_JS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Display which binaries we're using
|
||||
echo "Using Node binary: $NODE_BIN"
|
||||
echo "Using NPM CLI JS: $NPM_CLI_JS"
|
||||
|
||||
# Remove existing node_modules directory and package files
|
||||
echo "Cleaning up existing installation..."
|
||||
rm -rf "$SCRIPT_DIR/node_modules"
|
||||
rm -f "$SCRIPT_DIR/package-lock.json"
|
||||
|
||||
# Back up existing package.json if it exists
|
||||
if [ -f "$SCRIPT_DIR/package.json" ]; then
|
||||
echo "Backing up existing package.json..."
|
||||
cp "$SCRIPT_DIR/package.json" "$SCRIPT_DIR/package.json.bak"
|
||||
fi
|
||||
|
||||
# Create a clean minimal package.json with ONLY net-snmp dependency
|
||||
echo "Creating minimal package.json with only net-snmp dependency..."
|
||||
VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json.bak" | head -1 | cut -d'"' -f4 || echo "2.6.3")
|
||||
echo '{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "'$VERSION'",
|
||||
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
|
||||
"main": "dist_ts/index.js",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"nupst": "bin/nupst"
|
||||
},
|
||||
"dependencies": {
|
||||
"net-snmp": "3.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"private": true
|
||||
}' > "$SCRIPT_DIR/package.json"
|
||||
|
||||
# Install ONLY net-snmp
|
||||
echo "Installing ONLY net-snmp dependency (+ 2 subdependencies)..."
|
||||
echo "Node version: $("$NODE_BIN" --version)"
|
||||
echo "Executing NPM directly with Node.js"
|
||||
|
||||
# Execute npm-cli.js directly with our Node.js binary
|
||||
"$NODE_BIN" "$NPM_CLI_JS" --prefix "$SCRIPT_DIR" install --no-audit --no-fund
|
||||
|
||||
INSTALL_STATUS=$?
|
||||
if [ $INSTALL_STATUS -ne 0 ]; then
|
||||
echo "Error: Failed to install net-snmp dependency. NUPST may not function correctly."
|
||||
echo "Restoring original package.json..."
|
||||
mv "$SCRIPT_DIR/package.json.bak" "$SCRIPT_DIR/package.json"
|
||||
exit 1
|
||||
else
|
||||
echo "net-snmp dependency installed successfully."
|
||||
# Show what's actually installed
|
||||
echo "Installed modules:"
|
||||
find "$SCRIPT_DIR/node_modules" -maxdepth 1 -type d | grep -v "^$SCRIPT_DIR/node_modules$" | sort
|
||||
|
||||
# Remove backup if successful
|
||||
rm -f "$SCRIPT_DIR/package.json.bak"
|
||||
fi
|
||||
|
||||
# No temporary files to clean up
|
||||
|
||||
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"
|
||||
|
147
test/test.logger.ts
Normal file
147
test/test.logger.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { Logger } from '../ts/logger.js';
|
||||
|
||||
// Create a Logger instance for testing
|
||||
const logger = new Logger();
|
||||
|
||||
tap.test('should create a logger instance', async () => {
|
||||
expect(logger instanceof Logger).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should log messages with different log levels', async () => {
|
||||
// We're not testing console output directly, just ensuring no errors
|
||||
logger.log('Regular log message');
|
||||
logger.error('Error message');
|
||||
logger.warn('Warning message');
|
||||
logger.success('Success message');
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should create a logbox with title, content, and end', async () => {
|
||||
// Just ensuring no errors occur
|
||||
logger.logBoxTitle('Test Box', 40);
|
||||
logger.logBoxLine('This is a test line');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should handle width persistence between logbox calls', async () => {
|
||||
logger.logBoxTitle('Width Test', 45);
|
||||
|
||||
// These should use the width from the title
|
||||
logger.logBoxLine('Line 1');
|
||||
logger.logBoxLine('Line 2');
|
||||
logger.logBoxEnd();
|
||||
|
||||
let errorThrown = false;
|
||||
|
||||
try {
|
||||
// This should work fine after the reset in logBoxEnd
|
||||
logger.logBoxTitle('New Box', 30);
|
||||
logger.logBoxLine('New line');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
}
|
||||
|
||||
expect(errorThrown).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should throw error when using logBoxLine without width', async () => {
|
||||
let errorThrown = false;
|
||||
let errorMessage = '';
|
||||
|
||||
try {
|
||||
// Should throw because no width is set
|
||||
logger.logBoxLine('This should fail');
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
errorMessage = (error as Error).message;
|
||||
}
|
||||
|
||||
expect(errorThrown).toBeTruthy();
|
||||
expect(errorMessage).toBeTruthy();
|
||||
expect(errorMessage.includes('No box width')).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should create a complete logbox in one call', async () => {
|
||||
// Just ensuring no errors occur
|
||||
logger.logBox('Complete Box', [
|
||||
'Line 1',
|
||||
'Line 2',
|
||||
'Line 3'
|
||||
], 40);
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should handle content that exceeds box width', async () => {
|
||||
// Just ensuring no errors occur when content is too long
|
||||
logger.logBox('Truncation Test', [
|
||||
'This line is way too long and should be truncated because it exceeds the available space'
|
||||
], 30);
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should create dividers with custom characters', async () => {
|
||||
// Just ensuring no errors occur
|
||||
logger.logDivider(30);
|
||||
logger.logDivider(20, '*');
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('Logger Demo', async () => {
|
||||
console.log('\n=== LOGGER DEMO ===\n');
|
||||
|
||||
// Basic logging
|
||||
logger.log('Regular log message');
|
||||
logger.error('Error message');
|
||||
logger.warn('Warning message');
|
||||
logger.success('Success message');
|
||||
|
||||
// Logbox with title, content lines, and end
|
||||
logger.logBoxTitle('Configuration Loaded', 50);
|
||||
logger.logBoxLine('SNMP Settings:');
|
||||
logger.logBoxLine(' Host: 127.0.0.1');
|
||||
logger.logBoxLine(' Port: 161');
|
||||
logger.logBoxLine(' Version: 1');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Complete logbox in one call
|
||||
logger.logBox('UPS Status', [
|
||||
'Power Status: onBattery',
|
||||
'Battery Capacity: 75%',
|
||||
'Runtime Remaining: 30 minutes'
|
||||
], 45);
|
||||
|
||||
// Logbox with content that's too long for the width
|
||||
logger.logBox('Truncation Example', [
|
||||
'This line is short enough to fit within the box width',
|
||||
'This line is way too long and will be truncated because it exceeds the available space for content within the logbox'
|
||||
], 40);
|
||||
|
||||
// Demonstrating logbox width being remembered
|
||||
logger.logBoxTitle('Width Persistence Example', 60);
|
||||
logger.logBoxLine('These lines use the width from the title');
|
||||
logger.logBoxLine('No need to specify the width again');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Divider example
|
||||
logger.log('\nDivider example:');
|
||||
logger.logDivider(30);
|
||||
logger.logDivider(30, '*');
|
||||
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
// Export the default tap object
|
||||
export default tap.start();
|
329
test/test.ts
329
test/test.ts
@ -1,9 +1,6 @@
|
||||
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 { NupstSnmp } from '../ts/snmp/manager.js';
|
||||
import type { ISnmpConfig, IUpsStatus } from '../ts/snmp/types.js';
|
||||
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
const testQenv = new qenv.Qenv('./', '.nogit/');
|
||||
@ -12,295 +9,57 @@ const testQenv = new qenv.Qenv('./', '.nogit/');
|
||||
const snmp = new NupstSnmp(true);
|
||||
|
||||
// Load the test configuration from .nogit/env.json
|
||||
const testConfig = await testQenv.getEnvVarOnDemandAsObject('testConfig');
|
||||
const testConfigV1 = await testQenv.getEnvVarOnDemandAsObject('testConfigV1');
|
||||
const testConfigV3 = await testQenv.getEnvVarOnDemandAsObject('testConfigV3');
|
||||
|
||||
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);
|
||||
console.log(testConfigV1);
|
||||
});
|
||||
|
||||
// Test with real UPS using the configuration from .nogit/env.json
|
||||
tap.test('Real UPS test', async () => {
|
||||
tap.test('Real UPS test v1', async () => {
|
||||
try {
|
||||
console.log('Testing with real UPS configuration...');
|
||||
|
||||
// Extract the correct SNMP config from the test configuration
|
||||
const snmpConfig = testConfig.snmp;
|
||||
const snmpConfig = testConfigV1.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');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Real UPS test v3', async () => {
|
||||
try {
|
||||
console.log('Testing with real UPS configuration...');
|
||||
|
||||
// Extract the correct SNMP config from the test configuration
|
||||
const snmpConfig = testConfigV3.snmp;
|
||||
console.log('SNMP Config:');
|
||||
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
||||
console.log(` Version: SNMPv${snmpConfig.version}`);
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/nupst',
|
||||
version: '2.4.6',
|
||||
version: '2.6.16',
|
||||
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
|
||||
}
|
||||
|
252
ts/daemon.ts
252
ts/daemon.ts
@ -1,10 +1,13 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { exec, execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { NupstSnmp, type ISnmpConfig } from './snmp.js';
|
||||
import { NupstSnmp } from './snmp/manager.js';
|
||||
import type { ISnmpConfig } from './snmp/types.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Configuration interface for the daemon
|
||||
@ -123,7 +126,7 @@ export class NupstDaemon {
|
||||
console.error('┌─ Configuration Error ─────────────────────┐');
|
||||
console.error(`│ ${message}`);
|
||||
console.error('│ Please run \'nupst setup\' first to create a configuration.');
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
console.error('└───────────────────────────────────────────┘');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -145,11 +148,11 @@ export class NupstDaemon {
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
console.log('Daemon is already running');
|
||||
logger.log('Daemon is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Starting NUPST daemon...');
|
||||
logger.log('Starting NUPST daemon...');
|
||||
|
||||
try {
|
||||
// Load configuration - this will throw an error if config doesn't exist
|
||||
@ -163,11 +166,12 @@ export class NupstDaemon {
|
||||
this.snmp.getNupst().checkForUpdates().then(updateAvailable => {
|
||||
if (updateAvailable) {
|
||||
const updateStatus = this.snmp.getNupst().getUpdateStatus();
|
||||
console.log('┌─ Update Available ───────────────────────┐');
|
||||
console.log(`│ Current Version: ${updateStatus.currentVersion}`);
|
||||
console.log(`│ Latest Version: ${updateStatus.latestVersion}`);
|
||||
console.log('│ Run "sudo nupst update" to update');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Update Available', boxWidth);
|
||||
logger.logBoxLine(`Current Version: ${updateStatus.currentVersion}`);
|
||||
logger.logBoxLine(`Latest Version: ${updateStatus.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}).catch(() => {}); // Ignore errors checking for updates
|
||||
|
||||
@ -176,7 +180,7 @@ export class NupstDaemon {
|
||||
await this.monitor();
|
||||
} catch (error) {
|
||||
this.isRunning = false;
|
||||
console.error(`Daemon failed to start: ${error.message}`);
|
||||
logger.error(`Daemon failed to start: ${error.message}`);
|
||||
process.exit(1); // Exit with error
|
||||
}
|
||||
}
|
||||
@ -185,23 +189,24 @@ export class NupstDaemon {
|
||||
* Log the loaded configuration settings
|
||||
*/
|
||||
private logConfigLoaded(): void {
|
||||
console.log('┌─ Configuration Loaded ─────────────────────┐');
|
||||
console.log('│ SNMP Settings:');
|
||||
console.log(`│ Host: ${this.config.snmp.host}`);
|
||||
console.log(`│ Port: ${this.config.snmp.port}`);
|
||||
console.log(`│ Version: ${this.config.snmp.version}`);
|
||||
console.log('│ Thresholds:');
|
||||
console.log(`│ Battery: ${this.config.thresholds.battery}%`);
|
||||
console.log(`│ Runtime: ${this.config.thresholds.runtime} minutes`);
|
||||
console.log(`│ Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('Configuration Loaded', boxWidth);
|
||||
logger.logBoxLine('SNMP Settings:');
|
||||
logger.logBoxLine(` Host: ${this.config.snmp.host}`);
|
||||
logger.logBoxLine(` Port: ${this.config.snmp.port}`);
|
||||
logger.logBoxLine(` Version: ${this.config.snmp.version}`);
|
||||
logger.logBoxLine('Thresholds:');
|
||||
logger.logBoxLine(` Battery: ${this.config.thresholds.battery}%`);
|
||||
logger.logBoxLine(` Runtime: ${this.config.thresholds.runtime} minutes`);
|
||||
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the monitoring daemon
|
||||
*/
|
||||
public stop(): void {
|
||||
console.log('Stopping NUPST daemon...');
|
||||
logger.log('Stopping NUPST daemon...');
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
@ -209,7 +214,7 @@ export class NupstDaemon {
|
||||
* Monitor the UPS status and trigger shutdown when necessary
|
||||
*/
|
||||
private async monitor(): Promise<void> {
|
||||
console.log('Starting UPS monitoring...');
|
||||
logger.log('Starting UPS monitoring...');
|
||||
|
||||
let lastStatus: 'online' | 'onBattery' | 'unknown' = 'unknown';
|
||||
let lastLogTime = 0; // Track when we last logged status
|
||||
@ -224,20 +229,22 @@ export class NupstDaemon {
|
||||
|
||||
// Log status changes
|
||||
if (status.powerStatus !== lastStatus) {
|
||||
console.log('┌──────────────────────────────────────────┐');
|
||||
console.log(`│ Power status changed: ${lastStatus} → ${status.powerStatus}`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const statusBoxWidth = 45;
|
||||
logger.logBoxTitle('Power Status Change', statusBoxWidth);
|
||||
logger.logBoxLine(`Status changed: ${lastStatus} → ${status.powerStatus}`);
|
||||
logger.logBoxEnd();
|
||||
lastStatus = status.powerStatus;
|
||||
lastLogTime = currentTime; // Reset log timer when status changes
|
||||
}
|
||||
// Log status periodically (at least every 5 minutes)
|
||||
else if (shouldLogStatus) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log('┌──────────────────────────────────────────┐');
|
||||
console.log(`│ [${timestamp}] Periodic Status Update`);
|
||||
console.log(`│ Power Status: ${status.powerStatus}`);
|
||||
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const periodicBoxWidth = 45;
|
||||
logger.logBoxTitle('Periodic Status Update', periodicBoxWidth);
|
||||
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
||||
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(`Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
|
||||
logger.logBoxEnd();
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
|
||||
@ -265,8 +272,8 @@ export class NupstDaemon {
|
||||
batteryCapacity: number,
|
||||
batteryRuntime: number
|
||||
}): Promise<void> {
|
||||
console.log('┌─ UPS Status ───────────────────────────────┐');
|
||||
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min │`);
|
||||
console.log('┌─ UPS Status ─────────────────────────────┐');
|
||||
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
|
||||
// Check battery threshold
|
||||
@ -291,29 +298,107 @@ export class NupstDaemon {
|
||||
* @param reason Reason for shutdown
|
||||
*/
|
||||
public async initiateShutdown(reason: string): Promise<void> {
|
||||
console.log(`Initiating system shutdown due to: ${reason}`);
|
||||
logger.log(`Initiating system shutdown due to: ${reason}`);
|
||||
|
||||
// Set a longer delay for shutdown to allow VMs and services to close
|
||||
const shutdownDelayMinutes = 5;
|
||||
|
||||
try {
|
||||
// Execute shutdown command with delay to allow for VM graceful shutdown
|
||||
const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`);
|
||||
console.log('Shutdown initiated:', stdout);
|
||||
console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
|
||||
// Find shutdown command in common system paths
|
||||
const shutdownPaths = [
|
||||
'/sbin/shutdown',
|
||||
'/usr/sbin/shutdown',
|
||||
'/bin/shutdown',
|
||||
'/usr/bin/shutdown'
|
||||
];
|
||||
|
||||
let shutdownCmd = '';
|
||||
for (const path of shutdownPaths) {
|
||||
try {
|
||||
if (fs.existsSync(path)) {
|
||||
shutdownCmd = path;
|
||||
logger.log(`Found shutdown command at: ${shutdownCmd}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue checking other paths
|
||||
}
|
||||
}
|
||||
|
||||
if (shutdownCmd) {
|
||||
// Execute shutdown command with delay to allow for VM graceful shutdown
|
||||
logger.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`);
|
||||
const { stdout } = await execFileAsync(shutdownCmd, [
|
||||
'-h',
|
||||
`+${shutdownDelayMinutes}`,
|
||||
`UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`
|
||||
]);
|
||||
logger.log(`Shutdown initiated: ${stdout}`);
|
||||
logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
|
||||
} else {
|
||||
// Try using the PATH to find shutdown
|
||||
try {
|
||||
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`, {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
logger.log(`Shutdown initiated: ${stdout}`);
|
||||
} catch (e) {
|
||||
throw new Error(`Shutdown command not found: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor UPS during shutdown and force immediate shutdown if battery gets too low
|
||||
console.log('Monitoring UPS during shutdown process...');
|
||||
logger.log('Monitoring UPS during shutdown process...');
|
||||
await this.monitorDuringShutdown();
|
||||
} catch (error) {
|
||||
console.error('Failed to initiate shutdown:', error);
|
||||
// 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);
|
||||
logger.error(`Failed to initiate shutdown: ${error}`);
|
||||
|
||||
// Try alternative shutdown methods
|
||||
const alternatives = [
|
||||
{ cmd: 'poweroff', args: ['--force'] },
|
||||
{ cmd: 'halt', args: ['-p'] },
|
||||
{ cmd: 'systemctl', args: ['poweroff'] },
|
||||
{ cmd: 'reboot', args: ['-p'] } // Some systems allow reboot -p for power off
|
||||
];
|
||||
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
// First check if command exists in common system paths
|
||||
const paths = [
|
||||
`/sbin/${alt.cmd}`,
|
||||
`/usr/sbin/${alt.cmd}`,
|
||||
`/bin/${alt.cmd}`,
|
||||
`/usr/bin/${alt.cmd}`
|
||||
];
|
||||
|
||||
let cmdPath = '';
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
cmdPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdPath) {
|
||||
logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
|
||||
await execFileAsync(cmdPath, alt.args);
|
||||
return; // Exit if successful
|
||||
} else {
|
||||
// Try using PATH environment
|
||||
logger.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`);
|
||||
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
return; // Exit if successful
|
||||
}
|
||||
} catch (altError) {
|
||||
logger.error(`Alternative method ${alt.cmd} failed: ${altError}`);
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
logger.error('All shutdown methods failed');
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,10 +430,79 @@ export class NupstDaemon {
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
|
||||
try {
|
||||
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"');
|
||||
// Find shutdown command in common system paths
|
||||
const shutdownPaths = [
|
||||
'/sbin/shutdown',
|
||||
'/usr/sbin/shutdown',
|
||||
'/bin/shutdown',
|
||||
'/usr/bin/shutdown'
|
||||
];
|
||||
|
||||
let shutdownCmd = '';
|
||||
for (const path of shutdownPaths) {
|
||||
if (fs.existsSync(path)) {
|
||||
shutdownCmd = path;
|
||||
console.log(`Found shutdown command at: ${shutdownCmd}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shutdownCmd) {
|
||||
console.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
|
||||
await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']);
|
||||
} else {
|
||||
// Try using the PATH to find shutdown
|
||||
console.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Emergency shutdown failed, trying alternative method...');
|
||||
await execAsync('poweroff --force');
|
||||
console.error('Emergency shutdown failed, trying alternative methods...');
|
||||
|
||||
// Try alternative shutdown methods in sequence
|
||||
const alternatives = [
|
||||
{ cmd: 'poweroff', args: ['--force'] },
|
||||
{ cmd: 'halt', args: ['-p'] },
|
||||
{ cmd: 'systemctl', args: ['poweroff'] }
|
||||
];
|
||||
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
// Check common paths
|
||||
const paths = [
|
||||
`/sbin/${alt.cmd}`,
|
||||
`/usr/sbin/${alt.cmd}`,
|
||||
`/bin/${alt.cmd}`,
|
||||
`/usr/bin/${alt.cmd}`
|
||||
];
|
||||
|
||||
let cmdPath = '';
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
cmdPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdPath) {
|
||||
console.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`);
|
||||
await execFileAsync(cmdPath, alt.args);
|
||||
return; // Exit if successful
|
||||
} else {
|
||||
// Try using PATH
|
||||
console.log(`Emergency: trying ${alt.cmd} via PATH`);
|
||||
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
||||
env: process.env
|
||||
});
|
||||
return; // Exit if successful
|
||||
}
|
||||
} catch (altError) {
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
console.error('All emergency shutdown methods failed');
|
||||
}
|
||||
|
||||
// Stop monitoring after initiating emergency shutdown
|
||||
|
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { NupstCli } from './cli.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* Main entry point for NUPST
|
||||
@ -13,6 +14,6 @@ async function main() {
|
||||
|
||||
// Run the main function and handle any errors
|
||||
main().catch(error => {
|
||||
console.error('Error:', error);
|
||||
logger.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
147
ts/logger.ts
Normal file
147
ts/logger.ts
Normal file
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* A simple logger class that provides consistent formatting for log messages
|
||||
* including support for logboxes with title, lines, and closing
|
||||
*/
|
||||
export class Logger {
|
||||
private currentBoxWidth: number | null = null;
|
||||
private static instance: Logger;
|
||||
|
||||
/**
|
||||
* Creates a new Logger instance
|
||||
*/
|
||||
constructor() {
|
||||
this.currentBoxWidth = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton logger instance
|
||||
* @returns The singleton logger instance
|
||||
*/
|
||||
public static getInstance(): Logger {
|
||||
if (!Logger.instance) {
|
||||
Logger.instance = new Logger();
|
||||
}
|
||||
return Logger.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message
|
||||
* @param message Message to log
|
||||
*/
|
||||
public log(message: string): void {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message
|
||||
* @param message Error message to log
|
||||
*/
|
||||
public error(message: string): void {
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning message with a warning emoji
|
||||
* @param message Warning message to log
|
||||
*/
|
||||
public warn(message: string): void {
|
||||
console.warn(`⚠️ ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a success message with a checkmark
|
||||
* @param message Success message to log
|
||||
*/
|
||||
public success(message: string): void {
|
||||
console.log(`✓ ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a logbox title and set the current box width
|
||||
* @param title Title of the logbox
|
||||
* @param width Width of the logbox (including borders)
|
||||
*/
|
||||
public logBoxTitle(title: string, width: number): void {
|
||||
this.currentBoxWidth = width;
|
||||
|
||||
// Create the title line with appropriate padding
|
||||
const paddedTitle = ` ${title} `;
|
||||
const remainingSpace = width - 3 - paddedTitle.length;
|
||||
|
||||
// Title line: ┌─ Title ───┐
|
||||
const titleLine = `┌─${paddedTitle}${'─'.repeat(remainingSpace)}┐`;
|
||||
|
||||
console.log(titleLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a logbox line
|
||||
* @param content Content of the line
|
||||
* @param width Optional width override. If not provided, uses the current box width.
|
||||
*/
|
||||
public logBoxLine(content: string, width?: number): void {
|
||||
const boxWidth = width || this.currentBoxWidth;
|
||||
|
||||
if (!boxWidth) {
|
||||
throw new Error('No box width specified and no previous box width to use');
|
||||
}
|
||||
|
||||
// Calculate the available space for content
|
||||
const availableSpace = boxWidth - 2; // Account for left and right borders
|
||||
|
||||
if (content.length <= availableSpace - 1) {
|
||||
// If content fits with at least one space for the right border stripe
|
||||
const padding = availableSpace - content.length - 1;
|
||||
console.log(`│ ${content}${' '.repeat(padding)}│`);
|
||||
} else {
|
||||
// Content is too long, let it flow out of boundaries.
|
||||
console.log(`│ ${content}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a logbox end
|
||||
* @param width Optional width override. If not provided, uses the current box width.
|
||||
*/
|
||||
public logBoxEnd(width?: number): void {
|
||||
const boxWidth = width || this.currentBoxWidth;
|
||||
|
||||
if (!boxWidth) {
|
||||
throw new Error('No box width specified and no previous box width to use');
|
||||
}
|
||||
|
||||
// Create the bottom border: └────────┘
|
||||
console.log(`└${'─'.repeat(boxWidth - 2)}┘`);
|
||||
|
||||
// Reset the current box width
|
||||
this.currentBoxWidth = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a complete logbox with title, content lines, and ending
|
||||
* @param title Title of the logbox
|
||||
* @param lines Array of content lines
|
||||
* @param width Width of the logbox
|
||||
*/
|
||||
public logBox(title: string, lines: string[], width: number): void {
|
||||
this.logBoxTitle(title, width);
|
||||
|
||||
for (const line of lines) {
|
||||
this.logBoxLine(line);
|
||||
}
|
||||
|
||||
this.logBoxEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a divider line
|
||||
* @param width Width of the divider
|
||||
* @param character Character to use for the divider (default: ─)
|
||||
*/
|
||||
public logDivider(width: number, character: string = '─'): void {
|
||||
console.log(character.repeat(width));
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance for easy use
|
||||
export const logger = Logger.getInstance();
|
34
ts/nupst.ts
34
ts/nupst.ts
@ -1,9 +1,10 @@
|
||||
import { NupstSnmp } from './snmp.js';
|
||||
import { NupstSnmp } from './snmp/manager.js';
|
||||
import { NupstDaemon } from './daemon.js';
|
||||
import { NupstSystemd } from './systemd.js';
|
||||
import { commitinfo } from './00_commitinfo_data.js';
|
||||
import { spawn } from 'child_process';
|
||||
import * as https from 'https';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* Main Nupst class that coordinates all components
|
||||
@ -70,7 +71,7 @@ export class Nupst {
|
||||
|
||||
return this.updateAvailable;
|
||||
} catch (error) {
|
||||
console.error(`Error checking for updates: ${error.message}`);
|
||||
logger.error(`Error checking for updates: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -162,28 +163,33 @@ export class Nupst {
|
||||
*/
|
||||
public logVersionInfo(checkForUpdates: boolean = true): void {
|
||||
const version = this.getVersion();
|
||||
console.log('┌─ NUPST Version ────────────────────────┐');
|
||||
console.log(`│ Current Version: ${version}`);
|
||||
const boxWidth = 45;
|
||||
|
||||
logger.logBoxTitle('NUPST Version', boxWidth);
|
||||
logger.logBoxLine(`Current Version: ${version}`);
|
||||
|
||||
if (this.updateAvailable && this.latestVersion) {
|
||||
console.log(`│ Update Available: ${this.latestVersion}`);
|
||||
console.log('│ Run "sudo nupst update" to update');
|
||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
logger.logBoxEnd();
|
||||
} else if (checkForUpdates) {
|
||||
console.log('│ Checking for updates...');
|
||||
logger.logBoxLine('Checking for updates...');
|
||||
|
||||
// We can't end the box yet since we're in an async operation
|
||||
this.checkForUpdates().then(updateAvailable => {
|
||||
if (updateAvailable) {
|
||||
console.log(`│ Update Available: ${this.latestVersion}`);
|
||||
console.log('│ Run "sudo nupst update" to update');
|
||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
} else {
|
||||
console.log('│ You are running the latest version');
|
||||
logger.logBoxLine('You are running the latest version');
|
||||
}
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
logger.logBoxEnd();
|
||||
}).catch(() => {
|
||||
console.log('│ Could not check for updates');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
logger.logBoxLine('Could not check for updates');
|
||||
logger.logBoxEnd();
|
||||
});
|
||||
} else {
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Re-export from the snmp module
|
||||
* This file is kept for backward compatibility
|
||||
*/
|
||||
|
||||
export * from './snmp/index.js';
|
@ -1,98 +0,0 @@
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
import * as dgram from 'dgram';
|
||||
import * as snmp from 'net-snmp';
|
||||
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js';
|
||||
import { UpsOidSets } from './oid-sets.js';
|
||||
import { SnmpPacketCreator } from './packet-creator.js';
|
||||
import { SnmpPacketParser } from './packet-parser.js';
|
||||
|
||||
/**
|
||||
* Class for SNMP communication with UPS devices
|
||||
@ -13,6 +11,8 @@ export class NupstSnmp {
|
||||
private activeOIDs: IOidSet;
|
||||
// Reference to the parent Nupst instance
|
||||
private nupst: any; // Type 'any' to avoid circular dependency
|
||||
// Debug mode flag
|
||||
private debug: boolean = false;
|
||||
|
||||
// Default SNMP configuration
|
||||
private readonly DEFAULT_CONFIG: ISnmpConfig = {
|
||||
@ -24,13 +24,6 @@ export class NupstSnmp {
|
||||
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
|
||||
@ -56,6 +49,14 @@ export class NupstSnmp {
|
||||
return this.nupst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable debug mode
|
||||
*/
|
||||
public enableDebug(): void {
|
||||
this.debug = true;
|
||||
console.log('SNMP debug mode enabled - detailed logs will be shown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set active OID set based on UPS model
|
||||
* @param config SNMP configuration
|
||||
@ -78,121 +79,190 @@ export class NupstSnmp {
|
||||
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
|
||||
* Send an SNMP GET request using the net-snmp package
|
||||
* @param oid OID to query
|
||||
* @param config SNMP configuration
|
||||
* @param retryCount Current retry count (unused in this implementation)
|
||||
* @returns Promise resolving to the SNMP response value
|
||||
*/
|
||||
public async snmpGet(oid: string, config = this.DEFAULT_CONFIG): Promise<any> {
|
||||
public async snmpGet(
|
||||
oid: string,
|
||||
config = this.DEFAULT_CONFIG,
|
||||
retryCount = 0
|
||||
): 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'));
|
||||
console.log(`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`);
|
||||
console.log('Using community:', config.community);
|
||||
}
|
||||
|
||||
// Create SNMP options based on configuration
|
||||
const options: any = {
|
||||
port: config.port,
|
||||
retries: 2, // Number of retries
|
||||
timeout: config.timeout,
|
||||
transport: 'udp4',
|
||||
idBitsSize: 32,
|
||||
context: config.context || ''
|
||||
};
|
||||
|
||||
// Set version based on config
|
||||
if (config.version === 1) {
|
||||
options.version = snmp.Version1;
|
||||
} else if (config.version === 2) {
|
||||
options.version = snmp.Version2c;
|
||||
} else {
|
||||
options.version = snmp.Version3;
|
||||
}
|
||||
|
||||
// Create appropriate session based on SNMP version
|
||||
let session;
|
||||
|
||||
// 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');
|
||||
if (config.version === 3) {
|
||||
// For SNMPv3, we need to set up authentication and privacy
|
||||
// For SNMPv3, we need a valid security level
|
||||
const securityLevel = config.securityLevel || 'noAuthNoPriv';
|
||||
|
||||
// Create the user object with required structure for net-snmp
|
||||
const user: any = {
|
||||
name: config.username || ''
|
||||
};
|
||||
|
||||
// Set security level
|
||||
if (securityLevel === 'noAuthNoPriv') {
|
||||
user.level = snmp.SecurityLevel.noAuthNoPriv;
|
||||
} else if (securityLevel === 'authNoPriv') {
|
||||
user.level = snmp.SecurityLevel.authNoPriv;
|
||||
|
||||
// Set auth protocol - must provide both protocol and key
|
||||
if (config.authProtocol && config.authKey) {
|
||||
if (config.authProtocol === 'MD5') {
|
||||
user.authProtocol = snmp.AuthProtocols.md5;
|
||||
} else if (config.authProtocol === 'SHA') {
|
||||
user.authProtocol = snmp.AuthProtocols.sha;
|
||||
}
|
||||
user.authKey = config.authKey;
|
||||
} else {
|
||||
// Fallback to noAuthNoPriv if auth details missing
|
||||
user.level = snmp.SecurityLevel.noAuthNoPriv;
|
||||
if (this.debug) {
|
||||
console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv');
|
||||
}
|
||||
}
|
||||
} else if (securityLevel === 'authPriv') {
|
||||
user.level = snmp.SecurityLevel.authPriv;
|
||||
|
||||
// Set auth protocol - must provide both protocol and key
|
||||
if (config.authProtocol && config.authKey) {
|
||||
if (config.authProtocol === 'MD5') {
|
||||
user.authProtocol = snmp.AuthProtocols.md5;
|
||||
} else if (config.authProtocol === 'SHA') {
|
||||
user.authProtocol = snmp.AuthProtocols.sha;
|
||||
}
|
||||
user.authKey = config.authKey;
|
||||
|
||||
// Set privacy protocol - must provide both protocol and key
|
||||
if (config.privProtocol && config.privKey) {
|
||||
if (config.privProtocol === 'DES') {
|
||||
user.privProtocol = snmp.PrivProtocols.des;
|
||||
} else if (config.privProtocol === 'AES') {
|
||||
user.privProtocol = snmp.PrivProtocols.aes;
|
||||
}
|
||||
user.privKey = config.privKey;
|
||||
} else {
|
||||
// Fallback to authNoPriv if priv details missing
|
||||
user.level = snmp.SecurityLevel.authNoPriv;
|
||||
if (this.debug) {
|
||||
console.log('Warning: Missing privProtocol or privKey, falling back to authNoPriv');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to noAuthNoPriv if auth details missing
|
||||
user.level = snmp.SecurityLevel.noAuthNoPriv;
|
||||
if (this.debug) {
|
||||
console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv');
|
||||
}
|
||||
}
|
||||
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'));
|
||||
console.log('SNMPv3 user configuration:', {
|
||||
name: user.name,
|
||||
level: Object.keys(snmp.SecurityLevel).find(key => snmp.SecurityLevel[key] === user.level),
|
||||
authProtocol: user.authProtocol ? 'Set' : 'Not Set',
|
||||
authKey: user.authKey ? 'Set' : 'Not Set',
|
||||
privProtocol: user.privProtocol ? 'Set' : 'Not Set',
|
||||
privKey: user.privKey ? 'Set' : 'Not Set'
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
session = snmp.createV3Session(config.host, user, options);
|
||||
} else {
|
||||
// For SNMPv1/v2c, we use the community string
|
||||
session = snmp.createSession(config.host, config.community || 'public', options);
|
||||
}
|
||||
|
||||
// Convert the OID string to an array of OIDs if multiple OIDs are needed
|
||||
const oids = [oid];
|
||||
|
||||
// Send the GET request
|
||||
session.get(oids, (error: any, varbinds: any[]) => {
|
||||
// Close the session to release resources
|
||||
session.close();
|
||||
|
||||
if (error) {
|
||||
clearTimeout(timeout);
|
||||
socket.close();
|
||||
if (this.debug) {
|
||||
console.error('Error sending SNMP request:', error);
|
||||
console.error('SNMP GET error:', error);
|
||||
}
|
||||
reject(error);
|
||||
} else if (this.debug) {
|
||||
console.log('SNMP request sent successfully');
|
||||
reject(new Error(`SNMP GET error: ${error.message || error}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!varbinds || varbinds.length === 0) {
|
||||
if (this.debug) {
|
||||
console.error('No varbinds returned in response');
|
||||
}
|
||||
reject(new Error('No varbinds returned in response'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for SNMP errors in the response
|
||||
if (varbinds[0].type === snmp.ObjectType.NoSuchObject ||
|
||||
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
|
||||
varbinds[0].type === snmp.ObjectType.EndOfMibView) {
|
||||
if (this.debug) {
|
||||
console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]);
|
||||
}
|
||||
reject(new Error(`SNMP error: ${snmp.ObjectType[varbinds[0].type]}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the response value based on its type
|
||||
let value = varbinds[0].value;
|
||||
|
||||
// Handle specific types that might need conversion
|
||||
if (Buffer.isBuffer(value)) {
|
||||
// If value is a Buffer, try to convert it to a string if it's printable ASCII
|
||||
const isPrintableAscii = value.every(byte => byte >= 32 && byte <= 126);
|
||||
if (isPrintableAscii) {
|
||||
value = value.toString();
|
||||
}
|
||||
} else if (typeof value === 'bigint') {
|
||||
// Convert BigInt to a normal number or string if needed
|
||||
value = Number(value);
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
console.log('SNMP response:', {
|
||||
oid: varbinds[0].oid,
|
||||
type: varbinds[0].type,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
resolve(value);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -230,142 +300,16 @@ export class NupstSnmp {
|
||||
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;
|
||||
const powerStatusValue = await this.getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status', config);
|
||||
const batteryCapacity = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity', config) || 0;
|
||||
const batteryRuntime = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime', config) || 0;
|
||||
|
||||
// Determine power status - handle different values for different UPS models
|
||||
let powerStatus: 'online' | 'onBattery' | 'unknown' = 'unknown';
|
||||
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
|
||||
|
||||
// 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`);
|
||||
}
|
||||
}
|
||||
// Convert to minutes for UPS models with different time units
|
||||
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
|
||||
|
||||
const result = {
|
||||
powerStatus,
|
||||
@ -399,109 +343,231 @@ export class NupstSnmp {
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover SNMP engine ID (for SNMPv3)
|
||||
* Sends a proper discovery message to get the engine ID from the device
|
||||
* Helper method to get SNMP value with retry and fallback logic
|
||||
* @param oid OID to query
|
||||
* @param description Description of the value for logging
|
||||
* @param config SNMP configuration
|
||||
* @returns Promise resolving to the discovered engine ID
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
public async discoverEngineId(config: ISnmpConfig): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = dgram.createSocket('udp4');
|
||||
|
||||
// Create a proper discovery message (SNMPv3 with noAuthNoPriv)
|
||||
const discoveryConfig: ISnmpConfig = {
|
||||
...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++);
|
||||
|
||||
private async getSNMPValueWithRetry(
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig
|
||||
): Promise<any> {
|
||||
if (oid === '') {
|
||||
if (this.debug) {
|
||||
console.log('Sending SNMPv3 discovery message');
|
||||
console.log('SNMPv3 Discovery message:', request.toString('hex'));
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
// If we're using SNMPv3, try with different security levels
|
||||
if (config.version === 3) {
|
||||
return await this.tryFallbackSecurityLevels(oid, description, config);
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
});
|
||||
// Try with standard OIDs as fallback
|
||||
if (config.upsModel !== 'custom') {
|
||||
return await this.tryStandardOids(oid, description, config);
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
// Return a default value if all attempts fail
|
||||
if (this.debug) {
|
||||
console.log(`Using default value 0 for ${description}`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// initiateShutdown method has been moved to the NupstDaemon class
|
||||
/**
|
||||
* Try fallback security levels for SNMPv3
|
||||
* @param oid OID to query
|
||||
* @param description Description of the value for logging
|
||||
* @param config SNMP configuration
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
private async tryFallbackSecurityLevels(
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig
|
||||
): Promise<any> {
|
||||
if (this.debug) {
|
||||
console.log(`Retrying ${description} with fallback security level...`);
|
||||
}
|
||||
|
||||
// Try with authNoPriv if current level is authPriv
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try with noAuthNoPriv as a last resort
|
||||
if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') {
|
||||
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' };
|
||||
try {
|
||||
if (this.debug) {
|
||||
console.log(`Retrying with noAuthNoPriv 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try standard OIDs as fallback
|
||||
* @param oid OID to query
|
||||
* @param description Description of the value for logging
|
||||
* @param config SNMP configuration
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
private async tryStandardOids(
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig
|
||||
): Promise<any> {
|
||||
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 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine power status based on UPS model and raw value
|
||||
* @param upsModel UPS model
|
||||
* @param powerStatusValue Raw power status value
|
||||
* @returns Standardized power status
|
||||
*/
|
||||
private determinePowerStatus(
|
||||
upsModel: TUpsModel | undefined,
|
||||
powerStatusValue: number
|
||||
): 'online' | 'onBattery' | 'unknown' {
|
||||
if (upsModel === 'cyberpower') {
|
||||
// CyberPower RMCARD205: upsBaseOutputStatus values
|
||||
// 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc.
|
||||
if (powerStatusValue === 2) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 3) {
|
||||
return 'onBattery';
|
||||
}
|
||||
} else if (upsModel === 'eaton') {
|
||||
// Eaton UPS: xupsOutputSource values
|
||||
// 3=normal/mains, 5=battery, etc.
|
||||
if (powerStatusValue === 3) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 5) {
|
||||
return 'onBattery';
|
||||
}
|
||||
} else if (upsModel === 'apc') {
|
||||
// APC UPS: upsBasicOutputStatus values
|
||||
// 2=online, 3=onBattery, etc.
|
||||
if (powerStatusValue === 2) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 3) {
|
||||
return 'onBattery';
|
||||
}
|
||||
} else {
|
||||
// Default interpretation for other UPS models
|
||||
if (powerStatusValue === 1) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 2) {
|
||||
return 'onBattery';
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Process runtime value based on UPS model
|
||||
* @param upsModel UPS model
|
||||
* @param batteryRuntime Raw battery runtime value
|
||||
* @returns Processed runtime in minutes
|
||||
*/
|
||||
private processRuntimeValue(
|
||||
upsModel: TUpsModel | undefined,
|
||||
batteryRuntime: number
|
||||
): number {
|
||||
if (this.debug) {
|
||||
console.log('Raw runtime value:', batteryRuntime);
|
||||
}
|
||||
|
||||
if (upsModel === 'cyberpower' && batteryRuntime > 0) {
|
||||
// CyberPower: TimeTicks is in 1/100 seconds, convert to minutes
|
||||
const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
|
||||
if (this.debug) {
|
||||
console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`);
|
||||
}
|
||||
return minutes;
|
||||
} else if (upsModel === 'eaton' && batteryRuntime > 0) {
|
||||
// Eaton: Runtime is in seconds, convert to minutes
|
||||
const minutes = Math.floor(batteryRuntime / 60);
|
||||
if (this.debug) {
|
||||
console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`);
|
||||
}
|
||||
return minutes;
|
||||
} else if (batteryRuntime > 10000) {
|
||||
// Generic conversion for large tick values (likely TimeTicks)
|
||||
const minutes = Math.floor(batteryRuntime / 6000);
|
||||
if (this.debug) {
|
||||
console.log(`Converting ${batteryRuntime} ticks to ${minutes} minutes`);
|
||||
}
|
||||
return minutes;
|
||||
}
|
||||
|
||||
return batteryRuntime;
|
||||
}
|
||||
}
|
@ -25,9 +25,9 @@ export class UpsOidSets {
|
||||
|
||||
// Eaton OIDs
|
||||
eaton: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.534.1.1.2.0', // Power status
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // Remaining runtime in minutes
|
||||
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery)
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
|
||||
},
|
||||
|
||||
// TrippLite OIDs
|
||||
|
@ -1,651 +0,0 @@
|
||||
import * as crypto from 'crypto';
|
||||
import type { ISnmpConfig, ISnmpV3SecurityParams } 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: ISnmpConfig,
|
||||
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: ISnmpV3SecurityParams = {
|
||||
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: ISnmpConfig): 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: ISnmpConfig, 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: ISnmpConfig, 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
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,553 +0,0 @@
|
||||
import type { ISnmpConfig } 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: ISnmpConfig, 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -45,6 +45,8 @@ export interface ISnmpConfig {
|
||||
version: number;
|
||||
/** Timeout in milliseconds */
|
||||
timeout: number;
|
||||
|
||||
context?: string;
|
||||
|
||||
// SNMPv1/v2c
|
||||
/** Community string for SNMPv1/v2c */
|
||||
|
108
ts/systemd.ts
108
ts/systemd.ts
@ -1,6 +1,7 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { NupstDaemon } from './daemon.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* Class for managing systemd service
|
||||
@ -47,10 +48,11 @@ WantedBy=multi-user.target
|
||||
try {
|
||||
await fs.access(configPath);
|
||||
} catch (error) {
|
||||
console.error('┌─ Configuration Error ─────────────────────┐');
|
||||
console.error(`│ No configuration file found at ${configPath}`);
|
||||
console.error('│ Please run \'nupst setup\' first to create a configuration.');
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('Configuration Error', boxWidth);
|
||||
logger.logBoxLine(`No configuration file found at ${configPath}`);
|
||||
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
||||
logger.logBoxEnd();
|
||||
throw new Error('Configuration not found');
|
||||
}
|
||||
}
|
||||
@ -66,23 +68,24 @@ WantedBy=multi-user.target
|
||||
|
||||
// Write the service file
|
||||
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
|
||||
console.log('┌─ Service Installation ─────────────────────┐');
|
||||
console.log(`│ Service file created at ${this.serviceFilePath}`);
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('Service Installation', boxWidth);
|
||||
logger.logBoxLine(`Service file created at ${this.serviceFilePath}`);
|
||||
|
||||
// Reload systemd daemon
|
||||
execSync('systemctl daemon-reload');
|
||||
console.log('│ Systemd daemon reloaded');
|
||||
logger.logBoxLine('Systemd daemon reloaded');
|
||||
|
||||
// Enable the service
|
||||
execSync('systemctl enable nupst.service');
|
||||
console.log('│ Service enabled to start on boot');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
logger.logBoxLine('Service enabled to start on boot');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
if (error.message === 'Configuration not found') {
|
||||
// Just rethrow the error as the message has already been displayed
|
||||
throw error;
|
||||
}
|
||||
console.error('Failed to install systemd service:', error);
|
||||
logger.error(`Failed to install systemd service: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -97,15 +100,16 @@ WantedBy=multi-user.target
|
||||
await this.checkConfigExists();
|
||||
|
||||
execSync('systemctl start nupst.service');
|
||||
console.log('┌─ Service Status ─────────────────────────┐');
|
||||
console.log('│ NUPST service started successfully');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
logger.logBoxLine('NUPST service started successfully');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
if (error.message === 'Configuration not found') {
|
||||
// Exit with error code since configuration is required
|
||||
process.exit(1);
|
||||
}
|
||||
console.error('Failed to start service:', error);
|
||||
logger.error(`Failed to start service: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -117,9 +121,9 @@ WantedBy=multi-user.target
|
||||
public async stop(): Promise<void> {
|
||||
try {
|
||||
execSync('systemctl stop nupst.service');
|
||||
console.log('NUPST service stopped');
|
||||
logger.success('NUPST service stopped');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop service:', error);
|
||||
logger.error(`Failed to stop service: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -132,9 +136,10 @@ WantedBy=multi-user.target
|
||||
try {
|
||||
// Enable debug mode if requested
|
||||
if (debugMode) {
|
||||
console.log('┌─ Debug Mode ─────────────────────────────┐');
|
||||
console.log('│ SNMP debugging enabled - detailed logs will be shown');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Debug Mode', boxWidth);
|
||||
logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
|
||||
logger.logBoxEnd();
|
||||
this.daemon.getNupstSnmp().enableDebug();
|
||||
}
|
||||
|
||||
@ -152,7 +157,7 @@ WantedBy=multi-user.target
|
||||
await this.displayServiceStatus();
|
||||
await this.displayUpsStatus();
|
||||
} catch (error) {
|
||||
console.error(`Failed to get status: ${error.message}`);
|
||||
logger.error(`Failed to get status: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,13 +168,18 @@ WantedBy=multi-user.target
|
||||
private async displayServiceStatus(): Promise<void> {
|
||||
try {
|
||||
const serviceStatus = execSync('systemctl status nupst.service').toString();
|
||||
console.log('┌─ Service Status ─────────────────────────┐');
|
||||
console.log(serviceStatus.split('\n').map(line => `│ ${line}`).join('\n'));
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
// Process each line of the status output
|
||||
serviceStatus.split('\n').forEach(line => {
|
||||
logger.logBoxLine(line);
|
||||
});
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
console.error('┌─ Service Status ─────────────────────────┐');
|
||||
console.error('│ Service is not running');
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
logger.logBoxLine('Service is not running');
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,22 +200,24 @@ WantedBy=multi-user.target
|
||||
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for status check
|
||||
};
|
||||
|
||||
console.log('┌─ Connecting to UPS... ────────────────────┐');
|
||||
console.log(`│ Host: ${config.snmp.host}:${config.snmp.port}`);
|
||||
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Connecting to UPS...', boxWidth);
|
||||
logger.logBoxLine(`Host: ${config.snmp.host}:${config.snmp.port}`);
|
||||
logger.logBoxLine(`UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
const status = await snmp.getUpsStatus(snmpConfig);
|
||||
|
||||
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('└──────────────────────────────────────────┘');
|
||||
logger.logBoxTitle('UPS Status', boxWidth);
|
||||
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`);
|
||||
logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
console.error('┌─ UPS Status ───────────────────────────────┐');
|
||||
console.error(`│ Failed to retrieve UPS status: ${error.message}`);
|
||||
console.error('└──────────────────────────────────────────┘');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('UPS Status', boxWidth);
|
||||
logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,10 +233,10 @@ WantedBy=multi-user.target
|
||||
|
||||
// Reload systemd daemon
|
||||
execSync('systemctl daemon-reload');
|
||||
console.log('Systemd daemon reloaded');
|
||||
console.log('NUPST service has been successfully uninstalled');
|
||||
logger.log('Systemd daemon reloaded');
|
||||
logger.success('NUPST service has been successfully uninstalled');
|
||||
} catch (error) {
|
||||
console.error('Failed to disable and uninstall service:', error);
|
||||
logger.error(`Failed to disable and uninstall service: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -235,11 +247,11 @@ WantedBy=multi-user.target
|
||||
*/
|
||||
private async stopService(): Promise<void> {
|
||||
try {
|
||||
console.log('Stopping NUPST service...');
|
||||
logger.log('Stopping NUPST service...');
|
||||
execSync('systemctl stop nupst.service');
|
||||
} catch (error) {
|
||||
// Service might not be running, that's okay
|
||||
console.log('Service was not running or could not be stopped');
|
||||
logger.log('Service was not running or could not be stopped');
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,10 +261,10 @@ WantedBy=multi-user.target
|
||||
*/
|
||||
private async disableService(): Promise<void> {
|
||||
try {
|
||||
console.log('Disabling NUPST service...');
|
||||
logger.log('Disabling NUPST service...');
|
||||
execSync('systemctl disable nupst.service');
|
||||
} catch (error) {
|
||||
console.log('Service was not enabled or could not be disabled');
|
||||
logger.log('Service was not enabled or could not be disabled');
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,11 +274,11 @@ WantedBy=multi-user.target
|
||||
*/
|
||||
private async removeServiceFile(): Promise<void> {
|
||||
if (await fs.stat(this.serviceFilePath).catch(() => null)) {
|
||||
console.log(`Removing service file ${this.serviceFilePath}...`);
|
||||
logger.log(`Removing service file ${this.serviceFilePath}...`);
|
||||
await fs.unlink(this.serviceFilePath);
|
||||
console.log('Service file removed');
|
||||
logger.log('Service file removed');
|
||||
} else {
|
||||
console.log('Service file did not exist');
|
||||
logger.log('Service file did not exist');
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user