Compare commits
137 Commits
Author | SHA1 | Date | |
---|---|---|---|
d14ba1dd65 | |||
7d595fa175 | |||
df417432b0 | |||
e5f1ebf343 | |||
3ff0dd7ac8 | |||
bb87316dd3 | |||
d6e0a1a274 | |||
95fa4f8b0b | |||
c2f2f1e2ee | |||
936f86c346 | |||
7ff1a7da36 | |||
a87710144c | |||
23fd5cc5cd | |||
fb4d776bdd | |||
88ad16c638 | |||
016681b77b | |||
49f7a7da8b | |||
f8269a1cb7 | |||
b37e1aae6c | |||
7076829747 | |||
1387ca262b | |||
684f034aee | |||
a63ec16d63 | |||
85f34cf96a | |||
4d28614e08 | |||
567c7be7c5 | |||
a897a7c780 | |||
accf137216 | |||
c3441946cb | |||
37ccbf58fd | |||
071ded9c41 | |||
b935087d50 | |||
e1383097b2 | |||
dff0ea610b | |||
4faa10c494 | |||
c2d39cc19a | |||
9ccbbbdc37 | |||
1705ffe2be | |||
968cbbd8fc | |||
a2ae9960b6 | |||
df6a44d5d9 | |||
9efcc4b437 | |||
5903ae71be | |||
a649c598ad | |||
5f4f3ecbc3 | |||
806f81c6a0 | |||
88e353eec6 | |||
80ff1b1230 | |||
1075335497 | |||
eafb5207a4 | |||
9969e0f703 | |||
ac4b2c95f3 | |||
c593d76ead | |||
01ccf2d080 | |||
0e55f22dad | |||
bd3042de25 | |||
456351ca34 | |||
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 | |||
aa52d5e9f6 | |||
623b7ee51f | |||
897e86ad60 | |||
ed78db20e2 | |||
bd00dfe02c | |||
55c040df82 | |||
e68654a022 | |||
89a5d23d2f | |||
f9aa1cfd2f | |||
e47f316d0a | |||
901127f784 | |||
dc4fd5afba | |||
a7ced10f92 | |||
9b9e009523 | |||
1819b6827a | |||
bd5b85f6b0 | |||
c7db209da7 | |||
bbb8f4a22c | |||
ebc6f65fa9 | |||
0a459f9cd0 | |||
cf231e9785 | |||
edce110c8a | |||
5eefe8cf40 | |||
ecfd171f97 | |||
70c16fa0a6 | |||
7ef38cf961 | |||
fce5a9bafd | |||
8ee21ea92b | |||
32f85aa46f | |||
0a8a52f334 | |||
08f537aefd | |||
8431ef91a8 | |||
9bfb948e5c | |||
f5988dcd07 |
179
.gitea/workflows/README.md
Normal file
179
.gitea/workflows/README.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Gitea Actions Workflows
|
||||||
|
|
||||||
|
This directory contains automated workflows for NUPST's CI/CD pipeline.
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### CI Workflow (`ci.yml`)
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
|
||||||
|
- Push to `main` branch
|
||||||
|
- Push to `migration/**` branches
|
||||||
|
- Pull requests to `main`
|
||||||
|
|
||||||
|
**Jobs:**
|
||||||
|
|
||||||
|
1. **Type Check & Lint**
|
||||||
|
- Runs `deno check` for TypeScript validation
|
||||||
|
- Runs `deno lint` (continues on error)
|
||||||
|
- Runs `deno fmt --check` (continues on error)
|
||||||
|
|
||||||
|
2. **Build Test (Current Platform)**
|
||||||
|
- Compiles for Linux x86_64 (host platform)
|
||||||
|
- Tests binary execution (`--version` and `help`)
|
||||||
|
|
||||||
|
3. **Build All Platforms** (Main/Tags only)
|
||||||
|
- Compiles all 5 platform binaries
|
||||||
|
- Uploads as artifacts (30-day retention)
|
||||||
|
- Only runs on `main` branch or tags
|
||||||
|
|
||||||
|
### Release Workflow (`release.yml`)
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
|
||||||
|
- Push tags matching `v*` (e.g., `v4.0.0`)
|
||||||
|
|
||||||
|
**Jobs:**
|
||||||
|
|
||||||
|
1. **Version Verification**
|
||||||
|
- Extracts version from tag
|
||||||
|
- Verifies `deno.json` version matches tag
|
||||||
|
- Fails if mismatch detected
|
||||||
|
|
||||||
|
2. **Compilation**
|
||||||
|
- Compiles binaries for all 5 platforms:
|
||||||
|
- `nupst-linux-x64` (Linux x86_64)
|
||||||
|
- `nupst-linux-arm64` (Linux ARM64)
|
||||||
|
- `nupst-macos-x64` (macOS Intel)
|
||||||
|
- `nupst-macos-arm64` (macOS Apple Silicon)
|
||||||
|
- `nupst-windows-x64.exe` (Windows x64)
|
||||||
|
|
||||||
|
3. **Checksums**
|
||||||
|
- Generates SHA256 checksums for all binaries
|
||||||
|
- Creates `SHA256SUMS.txt`
|
||||||
|
|
||||||
|
4. **Release Creation**
|
||||||
|
- Creates Gitea release with tag
|
||||||
|
- Extracts release notes from CHANGELOG.md (if exists)
|
||||||
|
- Uploads all binaries + checksums as release assets
|
||||||
|
|
||||||
|
## Creating a Release
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. Update version in `deno.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "4.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update `CHANGELOG.md` with release notes (optional but recommended)
|
||||||
|
|
||||||
|
3. Commit all changes:
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "chore: bump version to 4.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release Process
|
||||||
|
|
||||||
|
1. Create and push a tag matching the version:
|
||||||
|
```bash
|
||||||
|
git tag v4.0.0
|
||||||
|
git push origin v4.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Gitea Actions will automatically:
|
||||||
|
- Verify version consistency
|
||||||
|
- Compile all platform binaries
|
||||||
|
- Generate checksums
|
||||||
|
- Create release with binaries attached
|
||||||
|
|
||||||
|
3. Monitor the workflow:
|
||||||
|
- Go to: `https://code.foss.global/serve.zone/nupst/actions`
|
||||||
|
- Check the "Release" workflow run
|
||||||
|
|
||||||
|
### Manual Release (Fallback)
|
||||||
|
|
||||||
|
If workflows fail, you can create a release manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compile all binaries
|
||||||
|
bash scripts/compile-all.sh
|
||||||
|
|
||||||
|
# Generate checksums
|
||||||
|
cd dist/binaries
|
||||||
|
sha256sum * > SHA256SUMS.txt
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Create release on Gitea UI
|
||||||
|
# Upload binaries manually
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Version Mismatch Error
|
||||||
|
|
||||||
|
If the release workflow fails with "Version mismatch":
|
||||||
|
|
||||||
|
- Ensure `deno.json` version matches the git tag
|
||||||
|
- Example: tag `v4.0.0` requires `"version": "4.0.0"` in deno.json
|
||||||
|
|
||||||
|
### Compilation Errors
|
||||||
|
|
||||||
|
If compilation fails:
|
||||||
|
|
||||||
|
1. Test locally: `bash scripts/compile-all.sh`
|
||||||
|
2. Check Deno version compatibility
|
||||||
|
3. Review TypeScript errors: `deno check mod.ts`
|
||||||
|
|
||||||
|
### Upload Failures
|
||||||
|
|
||||||
|
If binary upload fails:
|
||||||
|
|
||||||
|
1. Check Gitea Actions permissions
|
||||||
|
2. Verify `GITHUB_TOKEN` secret exists (auto-provided by Gitea)
|
||||||
|
3. Try manual release creation
|
||||||
|
|
||||||
|
## Workflow Secrets
|
||||||
|
|
||||||
|
The workflows use the following secrets:
|
||||||
|
|
||||||
|
- `GITHUB_TOKEN` - Auto-provided by Gitea Actions (no setup needed)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Testing Workflows Locally
|
||||||
|
|
||||||
|
You can test compilation locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Deno
|
||||||
|
curl -fsSL https://deno.land/install.sh | sh
|
||||||
|
|
||||||
|
# Test type checking
|
||||||
|
deno check mod.ts
|
||||||
|
|
||||||
|
# Test compilation
|
||||||
|
bash scripts/compile-all.sh
|
||||||
|
|
||||||
|
# Test binary
|
||||||
|
./dist/binaries/nupst-linux-x64 --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modifying Workflows
|
||||||
|
|
||||||
|
After modifying workflows:
|
||||||
|
|
||||||
|
1. Test syntax: Use a YAML validator
|
||||||
|
2. Commit changes: `git add .gitea/workflows/`
|
||||||
|
3. Push to feature branch first to test CI
|
||||||
|
4. Merge to main once verified
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Gitea Actions Documentation: https://docs.gitea.com/usage/actions/overview
|
||||||
|
- Deno Compile Documentation: https://docs.deno.com/runtime/manual/tools/compiler
|
||||||
|
- NUPST Repository: https://code.foss.global/serve.zone/nupst
|
84
.gitea/workflows/ci.yml
Normal file
84
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'migration/**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Type Check & Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Deno
|
||||||
|
uses: denoland/setup-deno@v1
|
||||||
|
with:
|
||||||
|
deno-version: v1.x
|
||||||
|
|
||||||
|
- name: Check TypeScript types
|
||||||
|
run: deno check mod.ts
|
||||||
|
|
||||||
|
- name: Lint code
|
||||||
|
run: deno lint
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: deno fmt --check
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build Test (Current Platform)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Deno
|
||||||
|
uses: denoland/setup-deno@v1
|
||||||
|
with:
|
||||||
|
deno-version: v1.x
|
||||||
|
|
||||||
|
- name: Compile for current platform
|
||||||
|
run: |
|
||||||
|
echo "Testing compilation for Linux x86_64..."
|
||||||
|
deno compile --allow-all --no-check \
|
||||||
|
--output nupst-test \
|
||||||
|
--target x86_64-unknown-linux-gnu mod.ts
|
||||||
|
|
||||||
|
- name: Test binary execution
|
||||||
|
run: |
|
||||||
|
chmod +x nupst-test
|
||||||
|
./nupst-test --version
|
||||||
|
./nupst-test help
|
||||||
|
|
||||||
|
build-all:
|
||||||
|
name: Build All Platforms
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Deno
|
||||||
|
uses: denoland/setup-deno@v1
|
||||||
|
with:
|
||||||
|
deno-version: v1.x
|
||||||
|
|
||||||
|
- name: Compile all platform binaries
|
||||||
|
run: bash scripts/compile-all.sh
|
||||||
|
|
||||||
|
- name: Upload all binaries as artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: nupst-binaries.zip
|
||||||
|
path: dist/binaries/*
|
||||||
|
retention-days: 30
|
249
.gitea/workflows/release.yml
Normal file
249
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Deno
|
||||||
|
uses: denoland/setup-deno@v1
|
||||||
|
with:
|
||||||
|
deno-version: v1.x
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Building version: $VERSION"
|
||||||
|
|
||||||
|
- name: Verify deno.json version matches tag
|
||||||
|
run: |
|
||||||
|
DENO_VERSION=$(grep -o '"version": "[^"]*"' deno.json | cut -d'"' -f4)
|
||||||
|
TAG_VERSION="${{ steps.version.outputs.version_number }}"
|
||||||
|
echo "deno.json version: $DENO_VERSION"
|
||||||
|
echo "Tag version: $TAG_VERSION"
|
||||||
|
if [ "$DENO_VERSION" != "$TAG_VERSION" ]; then
|
||||||
|
echo "ERROR: Version mismatch!"
|
||||||
|
echo "deno.json has version $DENO_VERSION but tag is $TAG_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Compile binaries for all platforms
|
||||||
|
run: |
|
||||||
|
echo "================================================"
|
||||||
|
echo " NUPST Release Compilation"
|
||||||
|
echo " Version: ${{ steps.version.outputs.version }}"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clean up old binaries and create fresh directory
|
||||||
|
rm -rf dist/binaries
|
||||||
|
mkdir -p dist/binaries
|
||||||
|
echo "→ Cleaned old binaries from dist/binaries"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Linux x86_64
|
||||||
|
echo "→ Compiling for Linux x86_64..."
|
||||||
|
deno compile --allow-all --no-check \
|
||||||
|
--output dist/binaries/nupst-linux-x64 \
|
||||||
|
--target x86_64-unknown-linux-gnu mod.ts
|
||||||
|
echo " ✓ Linux x86_64 complete"
|
||||||
|
|
||||||
|
# Linux ARM64
|
||||||
|
echo "→ Compiling for Linux ARM64..."
|
||||||
|
deno compile --allow-all --no-check \
|
||||||
|
--output dist/binaries/nupst-linux-arm64 \
|
||||||
|
--target aarch64-unknown-linux-gnu mod.ts
|
||||||
|
echo " ✓ Linux ARM64 complete"
|
||||||
|
|
||||||
|
# macOS x86_64
|
||||||
|
echo "→ Compiling for macOS x86_64..."
|
||||||
|
deno compile --allow-all --no-check \
|
||||||
|
--output dist/binaries/nupst-macos-x64 \
|
||||||
|
--target x86_64-apple-darwin mod.ts
|
||||||
|
echo " ✓ macOS x86_64 complete"
|
||||||
|
|
||||||
|
# macOS ARM64
|
||||||
|
echo "→ Compiling for macOS ARM64..."
|
||||||
|
deno compile --allow-all --no-check \
|
||||||
|
--output dist/binaries/nupst-macos-arm64 \
|
||||||
|
--target aarch64-apple-darwin mod.ts
|
||||||
|
echo " ✓ macOS ARM64 complete"
|
||||||
|
|
||||||
|
# Windows x86_64
|
||||||
|
echo "→ Compiling for Windows x86_64..."
|
||||||
|
deno compile --allow-all --no-check \
|
||||||
|
--output dist/binaries/nupst-windows-x64.exe \
|
||||||
|
--target x86_64-pc-windows-msvc mod.ts
|
||||||
|
echo " ✓ Windows x86_64 complete"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "All binaries compiled successfully!"
|
||||||
|
ls -lh dist/binaries/
|
||||||
|
|
||||||
|
- name: Generate SHA256 checksums
|
||||||
|
run: |
|
||||||
|
cd dist/binaries
|
||||||
|
sha256sum * > SHA256SUMS.txt
|
||||||
|
cat SHA256SUMS.txt
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
- name: Extract changelog for this version
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
# Check if CHANGELOG.md exists
|
||||||
|
if [ ! -f CHANGELOG.md ]; then
|
||||||
|
echo "No CHANGELOG.md found, using default release notes"
|
||||||
|
cat > /tmp/release_notes.md << EOF
|
||||||
|
## NUPST $VERSION
|
||||||
|
|
||||||
|
Pre-compiled binaries for multiple platforms.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Use the installation script:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Or download the binary for your platform and make it executable.
|
||||||
|
|
||||||
|
### Supported Platforms
|
||||||
|
- Linux x86_64 (x64)
|
||||||
|
- Linux ARM64 (aarch64)
|
||||||
|
- macOS x86_64 (Intel)
|
||||||
|
- macOS ARM64 (Apple Silicon)
|
||||||
|
- Windows x86_64
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
SHA256 checksums are provided in SHA256SUMS.txt
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
# Try to extract section for this version from CHANGELOG.md
|
||||||
|
# This is a simple extraction - adjust based on your CHANGELOG format
|
||||||
|
awk "/## \[$VERSION\]/,/## \[/" CHANGELOG.md | sed '$d' > /tmp/release_notes.md || cat > /tmp/release_notes.md << EOF
|
||||||
|
## NUPST $VERSION
|
||||||
|
|
||||||
|
See CHANGELOG.md for full details.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Use the installation script:
|
||||||
|
\`\`\`bash
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
|
\`\`\`
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Release notes:"
|
||||||
|
cat /tmp/release_notes.md
|
||||||
|
|
||||||
|
- name: Delete existing release if it exists
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
echo "Checking for existing release $VERSION..."
|
||||||
|
|
||||||
|
# Try to get existing release by tag
|
||||||
|
EXISTING_RELEASE_ID=$(curl -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/tags/$VERSION" \
|
||||||
|
| jq -r '.id // empty')
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_RELEASE_ID" ]; then
|
||||||
|
echo "Found existing release (ID: $EXISTING_RELEASE_ID), deleting..."
|
||||||
|
curl -X DELETE -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/$EXISTING_RELEASE_ID"
|
||||||
|
echo "Existing release deleted"
|
||||||
|
sleep 2
|
||||||
|
else
|
||||||
|
echo "No existing release found, proceeding with creation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
RELEASE_NOTES=$(cat /tmp/release_notes.md)
|
||||||
|
|
||||||
|
# Create the release
|
||||||
|
echo "Creating release for $VERSION..."
|
||||||
|
RELEASE_ID=$(curl -X POST -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases" \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"$VERSION\",
|
||||||
|
\"name\": \"NUPST $VERSION\",
|
||||||
|
\"body\": $(jq -Rs . /tmp/release_notes.md),
|
||||||
|
\"draft\": false,
|
||||||
|
\"prerelease\": false
|
||||||
|
}" | jq -r '.id')
|
||||||
|
|
||||||
|
echo "Release created with ID: $RELEASE_ID"
|
||||||
|
|
||||||
|
# Upload binaries as release assets
|
||||||
|
for binary in dist/binaries/*; do
|
||||||
|
filename=$(basename "$binary")
|
||||||
|
echo "Uploading $filename..."
|
||||||
|
curl -X POST -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@$binary" \
|
||||||
|
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/$RELEASE_ID/assets?name=$filename"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "All assets uploaded successfully"
|
||||||
|
|
||||||
|
- name: Clean up old releases
|
||||||
|
run: |
|
||||||
|
echo "Cleaning up old releases (keeping only last 3)..."
|
||||||
|
|
||||||
|
# Fetch all releases sorted by creation date
|
||||||
|
RELEASES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases" | \
|
||||||
|
jq -r 'sort_by(.created_at) | reverse | .[3:] | .[].id')
|
||||||
|
|
||||||
|
# Delete old releases
|
||||||
|
if [ -n "$RELEASES" ]; then
|
||||||
|
echo "Found releases to delete:"
|
||||||
|
for release_id in $RELEASES; do
|
||||||
|
echo " Deleting release ID: $release_id"
|
||||||
|
curl -X DELETE -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/$release_id"
|
||||||
|
done
|
||||||
|
echo "Old releases deleted successfully"
|
||||||
|
else
|
||||||
|
echo "No old releases to delete (less than 4 releases total)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
- name: Release Summary
|
||||||
|
run: |
|
||||||
|
echo "================================================"
|
||||||
|
echo " Release ${{ steps.version.outputs.version }} Complete!"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
echo "Binaries published:"
|
||||||
|
ls -lh dist/binaries/
|
||||||
|
echo ""
|
||||||
|
echo "Release URL:"
|
||||||
|
echo "https://code.foss.global/serve.zone/nupst/releases/tag/${{ steps.version.outputs.version }}"
|
||||||
|
echo ""
|
||||||
|
echo "Installation command:"
|
||||||
|
echo "curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
|
||||||
|
echo ""
|
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,15 +1,18 @@
|
|||||||
# Build
|
# Compiled Deno binaries (built by scripts/compile-all.sh)
|
||||||
dist*/
|
dist/binaries/
|
||||||
|
|
||||||
# Dependencies
|
# Deno cache and lock file
|
||||||
|
.deno/
|
||||||
|
deno.lock
|
||||||
|
|
||||||
|
# Legacy Node.js artifacts (v3.x and earlier - kept for safety)
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Bundled Node.js binaries
|
|
||||||
vendor/
|
vendor/
|
||||||
|
dist_ts/
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
@@ -18,4 +21,5 @@ npm-debug.log*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
.nogit/
|
# Development
|
||||||
|
.nogit/
|
||||||
|
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/cache
|
71
.serena/project.yml
Normal file
71
.serena/project.yml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||||
|
# * For C, use cpp
|
||||||
|
# * For JavaScript, use typescript
|
||||||
|
# Special requirements:
|
||||||
|
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
language: typescript
|
||||||
|
|
||||||
|
# the encoding used by text files in the project
|
||||||
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
|
encoding: 'utf-8'
|
||||||
|
|
||||||
|
# whether to use the project's gitignore file to ignore files
|
||||||
|
# Added on 2025-04-07
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
# list of additional paths to ignore
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||||
|
# Added (renamed) on 2025-04-07
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ''
|
||||||
|
|
||||||
|
project_name: 'nupst'
|
49
bin/nupst
49
bin/nupst
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# NUPST Launcher Script
|
|
||||||
# This script detects architecture and OS, then runs NUPST with the appropriate Node.js binary
|
|
||||||
|
|
||||||
# First, handle symlinks correctly
|
|
||||||
REAL_SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")
|
|
||||||
SCRIPT_DIR=$(dirname "$REAL_SCRIPT_PATH")
|
|
||||||
|
|
||||||
# For debugging
|
|
||||||
# echo "Script path: $REAL_SCRIPT_PATH"
|
|
||||||
# echo "Script dir: $SCRIPT_DIR"
|
|
||||||
|
|
||||||
# If we're run via symlink from /usr/local/bin, use the hardcoded installation path
|
|
||||||
if [[ "$SCRIPT_DIR" == "/usr/local/bin" ]]; then
|
|
||||||
PROJECT_ROOT="/opt/nupst"
|
|
||||||
else
|
|
||||||
# Otherwise, use relative path from script location
|
|
||||||
PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For debugging
|
|
||||||
# echo "Project root: $PROJECT_ROOT"
|
|
||||||
|
|
||||||
# Set Node.js binary path directly
|
|
||||||
NODE_BIN="$PROJECT_ROOT/vendor/node-linux-x64/bin/node"
|
|
||||||
|
|
||||||
# If binary doesn't exist, try system Node as fallback
|
|
||||||
if [ ! -f "$NODE_BIN" ]; then
|
|
||||||
if command -v node &> /dev/null; then
|
|
||||||
NODE_BIN="node"
|
|
||||||
echo "Using system Node.js installation"
|
|
||||||
else
|
|
||||||
echo "Error: Node.js binary not found at $NODE_BIN"
|
|
||||||
echo "Please run the setup script or install Node.js manually."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run NUPST with the Node.js binary
|
|
||||||
if [ -f "$PROJECT_ROOT/dist_ts/index.js" ]; then
|
|
||||||
exec "$NODE_BIN" "$PROJECT_ROOT/dist_ts/index.js" "$@"
|
|
||||||
elif [ -f "$PROJECT_ROOT/dist/index.js" ]; then
|
|
||||||
exec "$NODE_BIN" "$PROJECT_ROOT/dist/index.js" "$@"
|
|
||||||
else
|
|
||||||
echo "Error: Could not find NUPST's index.js file."
|
|
||||||
echo "Please run the setup script to download the required files."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
636
changelog.md
636
changelog.md
@@ -1,6 +1,560 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-10-18 - 4.0.0 - BREAKING CHANGE(core): Complete migration to Deno runtime
|
||||||
|
|
||||||
|
**MAJOR RELEASE: NUPST v4.0 is a complete rewrite powered by Deno**
|
||||||
|
|
||||||
|
This release fundamentally changes NUPST's architecture from Node.js-based to Deno-based,
|
||||||
|
distributed as pre-compiled binaries. This is a **breaking change** in terms of installation and
|
||||||
|
distribution, but configuration files from v3.x are **fully compatible**.
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
**Installation & Distribution:**
|
||||||
|
|
||||||
|
- **Removed**: Node.js runtime dependency - NUPST no longer requires Node.js
|
||||||
|
- **Removed**: npm package distribution (no longer published to npmjs.org)
|
||||||
|
- **Removed**: `bin/nupst` wrapper script
|
||||||
|
- **Removed**: `setup.sh` dependency installation
|
||||||
|
- **Removed**: All Node.js-related files (package.json, tsconfig.json, pnpm-lock.yaml,
|
||||||
|
npmextra.json)
|
||||||
|
- **Changed**: Installation now downloads pre-compiled binaries instead of cloning repository
|
||||||
|
- **Changed**: Binary-based distribution (~340MB self-contained executables)
|
||||||
|
|
||||||
|
**CLI Structure (Backward Compatible):**
|
||||||
|
|
||||||
|
- **Changed**: Commands now use subcommand structure (e.g., `nupst service enable` instead of
|
||||||
|
`nupst enable`)
|
||||||
|
- **Maintained**: Old command format still works with deprecation warnings for smooth migration
|
||||||
|
- **Added**: Aliases for common commands (`nupst ls`, `nupst rm`)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
**Distribution & Installation:**
|
||||||
|
|
||||||
|
- Pre-compiled binaries for 5 platforms:
|
||||||
|
- Linux x86_64
|
||||||
|
- Linux ARM64
|
||||||
|
- macOS x86_64 (Intel)
|
||||||
|
- macOS ARM64 (Apple Silicon)
|
||||||
|
- Windows x86_64
|
||||||
|
- Automated binary releases via Gitea Actions
|
||||||
|
- SHA256 checksum verification for all releases
|
||||||
|
- Installation from Gitea releases via updated `install.sh`
|
||||||
|
- Zero dependencies - completely self-contained binaries
|
||||||
|
- Cross-platform compilation from single codebase
|
||||||
|
|
||||||
|
**CI/CD Automation:**
|
||||||
|
|
||||||
|
- Gitea Actions workflows for continuous integration
|
||||||
|
- Automated release workflow triggered by git tags
|
||||||
|
- Automatic binary compilation for all platforms on release
|
||||||
|
- Type checking and linting in CI pipeline
|
||||||
|
- Build verification on every push
|
||||||
|
|
||||||
|
**CLI Improvements:**
|
||||||
|
|
||||||
|
- New hierarchical command structure with subcommands
|
||||||
|
- `nupst service` - Service management (enable, disable, start, stop, restart, status, logs)
|
||||||
|
- `nupst ups` - UPS device management (add, edit, remove, list, test)
|
||||||
|
- `nupst group` - Group management (add, edit, remove, list)
|
||||||
|
- `nupst config show` - Display configuration
|
||||||
|
- `nupst --version` - Show version information
|
||||||
|
- Better help messages organized by category
|
||||||
|
- Backward compatibility maintained with deprecation warnings
|
||||||
|
|
||||||
|
**Technical Improvements:**
|
||||||
|
|
||||||
|
- Deno runtime for modern TypeScript/JavaScript execution
|
||||||
|
- Native TypeScript support without compilation step
|
||||||
|
- Faster startup and execution compared to Node.js
|
||||||
|
- Smaller memory footprint
|
||||||
|
- Built-in permissions system
|
||||||
|
- No build step required for development
|
||||||
|
|
||||||
|
### Migration Guide
|
||||||
|
|
||||||
|
**For Users:**
|
||||||
|
|
||||||
|
1. Stop existing v3.x service: `sudo nupst disable`
|
||||||
|
2. Install v4.0 using new installer:
|
||||||
|
`curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y`
|
||||||
|
3. Your configuration at `/etc/nupst/config.json` is preserved and fully compatible
|
||||||
|
4. Enable service with new CLI: `sudo nupst service enable && sudo nupst service start`
|
||||||
|
5. Update systemd commands to use new syntax (old syntax still works with warnings)
|
||||||
|
|
||||||
|
**Configuration Compatibility:**
|
||||||
|
|
||||||
|
- All configuration files from v3.x work without modification
|
||||||
|
- No changes to `/etc/nupst/config.json` format
|
||||||
|
- All SNMP settings, thresholds, and group configurations preserved
|
||||||
|
|
||||||
|
**Command Mapping:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Old (v3.x) → New (v4.0)
|
||||||
|
nupst enable → nupst service enable
|
||||||
|
nupst disable → nupst service disable
|
||||||
|
nupst start → nupst service start
|
||||||
|
nupst stop → nupst service stop
|
||||||
|
nupst status → nupst service status
|
||||||
|
nupst logs → nupst service logs
|
||||||
|
nupst add → nupst ups add
|
||||||
|
nupst edit [id] → nupst ups edit [id]
|
||||||
|
nupst delete <id> → nupst ups remove <id>
|
||||||
|
nupst list → nupst ups list
|
||||||
|
nupst test → nupst ups test
|
||||||
|
nupst group add → nupst group add (unchanged)
|
||||||
|
nupst group edit <id> → nupst group edit <id> (unchanged)
|
||||||
|
nupst group delete <id> → nupst group remove <id>
|
||||||
|
nupst group list → nupst group list (unchanged)
|
||||||
|
nupst config → nupst config show
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
**Commit History:**
|
||||||
|
|
||||||
|
- `df6a44d`: Complete migration with Gitea Actions workflows and install.sh updates
|
||||||
|
- `9efcc4b`: CLI reorganization with subcommand structure
|
||||||
|
- `5903ae7`: Cross-platform compilation scripts
|
||||||
|
- `a649c59`: Deno migration with npm: and node: specifiers
|
||||||
|
- `5f4f3ec`: Initial migration to Deno
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
|
||||||
|
- Removed: 11 files (package.json, tsconfig.json, pnpm-lock.yaml, npmextra.json, bin/nupst,
|
||||||
|
setup.sh)
|
||||||
|
- Added: 3 Gitea Actions workflows (ci.yml, release.yml, README.md)
|
||||||
|
- Modified: 14 TypeScript files for Deno compatibility
|
||||||
|
- Updated: install.sh, .gitignore, readme.md
|
||||||
|
- Net reduction: -10,242 lines (93% reduction in repository size)
|
||||||
|
|
||||||
|
**Dependencies:**
|
||||||
|
|
||||||
|
- Runtime: Deno v1.x (bundled in binary, no installation required)
|
||||||
|
- SNMP: npm:net-snmp@3.20.0 (bundled in binary via npm: specifier)
|
||||||
|
- Node.js built-ins: Accessed via node: specifier (node:fs, node:child_process, etc.)
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
|
||||||
|
**For Users:**
|
||||||
|
|
||||||
|
- **Faster Installation**: Download single binary instead of cloning repo + installing Node.js + npm
|
||||||
|
dependencies
|
||||||
|
- **Zero Dependencies**: No Node.js or npm required on target system
|
||||||
|
- **Smaller Footprint**: Single binary vs repo + Node.js + node_modules
|
||||||
|
- **Easier Updates**: Download new binary instead of git pull + npm install
|
||||||
|
- **Better Security**: No npm supply chain risks, binary checksums provided
|
||||||
|
- **Platform Support**: Official binaries for all major platforms
|
||||||
|
|
||||||
|
**For Developers:**
|
||||||
|
|
||||||
|
- **Modern Tooling**: Native TypeScript support without build configuration
|
||||||
|
- **Faster Development**: No compilation step during development
|
||||||
|
- **CI/CD Automation**: Automated releases and testing
|
||||||
|
- **Cleaner Codebase**: 93% reduction in configuration files
|
||||||
|
- **Cross-Platform**: Compile for all platforms from any platform
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
|
||||||
|
- Windows ARM64 not supported (Deno limitation)
|
||||||
|
- Binary sizes are larger (~340MB) due to bundled runtime (trade-off for zero dependencies)
|
||||||
|
- First-time execution may be slower on some systems (binary extraction)
|
||||||
|
|
||||||
|
### Acknowledgments
|
||||||
|
|
||||||
|
This release represents a complete modernization of NUPST's infrastructure while maintaining full
|
||||||
|
backward compatibility for user configurations. Special thanks to the Deno team for creating an
|
||||||
|
excellent runtime that made this migration possible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2025-03-28 - 3.1.2 - fix(cli/ups-handler)
|
||||||
|
|
||||||
|
Improve UPS device listing table formatting for better column alignment
|
||||||
|
|
||||||
|
- Adjusted header spacing for the Host column and overall table alignment in the UPS handler output.
|
||||||
|
|
||||||
|
## 2025-03-28 - 3.1.1 - fix(cli)
|
||||||
|
|
||||||
|
Improve table header formatting in group and UPS listings
|
||||||
|
|
||||||
|
- Adjusted column padding in group listing for proper alignment
|
||||||
|
- Fixed UPS table header spacing for consistent CLI output
|
||||||
|
|
||||||
|
## 2025-03-28 - 3.1.0 - feat(cli)
|
||||||
|
|
||||||
|
Refactor CLI commands to use dedicated handlers for UPS, group, and service management
|
||||||
|
|
||||||
|
- Extracted UPS-related CLI logic into a new UpsHandler
|
||||||
|
- Introduced GroupHandler to manage UPS groups commands
|
||||||
|
- Added ServiceHandler for systemd service operations
|
||||||
|
- Updated CLI routing in cli.ts to delegate commands to the new handlers
|
||||||
|
- Exposed getters for the new handlers in the Nupst class
|
||||||
|
|
||||||
|
## 2025-03-28 - 3.0.1 - fix(cli)
|
||||||
|
|
||||||
|
Simplify UPS ID generation by removing the redundant promptForUniqueUpsId function in the CLI module
|
||||||
|
and replacing it with the shortId helper.
|
||||||
|
|
||||||
|
- Deleted the unused promptForUniqueUpsId method from ts/cli.ts.
|
||||||
|
- Updated UPS configuration to generate a unique ID directly using helpers.shortId().
|
||||||
|
- Improved code clarity by removing unnecessary interactive prompts for UPS IDs.
|
||||||
|
|
||||||
|
## 2025-03-28 - 3.0.0 - BREAKING CHANGE(core)
|
||||||
|
|
||||||
|
Add multi-UPS support and group management; update CLI, configuration and documentation to support
|
||||||
|
multiple UPS devices with group modes
|
||||||
|
|
||||||
|
- Implemented multi-UPS configuration with an array of UPS devices and groups in the configuration
|
||||||
|
file
|
||||||
|
- Added group management commands (group add, edit, delete, list) with redundant and non-redundant
|
||||||
|
modes
|
||||||
|
- Revamped CLI command parsing for UPS management (add, edit, delete, list, setup) and group
|
||||||
|
subcommands
|
||||||
|
- Updated readme and documentation to reflect new configuration structure and features
|
||||||
|
- Enhanced logging and status display for multiple UPS devices
|
||||||
|
|
||||||
|
## 2025-03-26 - 2.6.17 - fix(logger)
|
||||||
|
|
||||||
|
Preserve logbox width after logBoxEnd so that subsequent logBoxLine calls continue using the set
|
||||||
|
width.
|
||||||
|
|
||||||
|
- Removed the reset of currentBoxWidth in logBoxEnd to allow persistent width across logbox calls.
|
||||||
|
- Ensures that logBoxLine uses the previously set width when no new width is provided.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Changed install.sh to require explicit download of the install script and updated error messages
|
||||||
|
for non-interactive modes
|
||||||
|
- Updated readme.md to include three distinct installation methods with clear command examples
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.4.5 - fix(install)
|
||||||
|
|
||||||
|
Improve interactive terminal detection and update installation instructions
|
||||||
|
|
||||||
|
- Enhanced install.sh to better detect non-interactive environments and provide clearer guidance for
|
||||||
|
both interactive and non-interactive installations
|
||||||
|
- Updated README.md quick install instructions to recommend process substitution and clarify
|
||||||
|
auto-yes usage
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.4.4 - fix(install)
|
||||||
|
|
||||||
|
Improve interactive mode detection and non-interactive installation handling in install.sh
|
||||||
|
|
||||||
|
- Detect and warn when running without a controlling terminal
|
||||||
|
- Attempt to use /dev/tty for user input when possible
|
||||||
|
- Update prompts and error messages for auto-installation of dependencies
|
||||||
|
- Clarify installation instructions in readme for interactive and non-interactive modes
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.4.3 - fix(readme)
|
||||||
|
|
||||||
|
Update Quick Install command syntax in readme for auto-yes installation
|
||||||
|
|
||||||
|
- Changed installation command to use: curl -sSL
|
||||||
|
https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -c "bash -s --
|
||||||
|
-y"
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.4.2 - fix(daemon)
|
||||||
|
|
||||||
|
Refactor shutdown initiation logic in daemon by moving the initiateShutdown and
|
||||||
|
monitorDuringShutdown methods from the SNMP manager to the daemon, and update calls accordingly
|
||||||
|
|
||||||
|
- Moved initiateShutdown and monitorDuringShutdown to the daemon class for improved cohesion
|
||||||
|
- Updated references in the daemon to call its own shutdown method instead of the SNMP manager
|
||||||
|
- Removed redundant initiateShutdown method from the SNMP manager
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.4.1 - fix(docs)
|
||||||
|
|
||||||
|
Update readme with detailed legal and trademark guidance
|
||||||
|
|
||||||
|
- Clarified legal section by adding trademark and company information
|
||||||
|
- Ensured users understand that licensing terms do not imply endorsement by the company
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.4.0 - feat(installer)
|
||||||
|
|
||||||
|
Add auto-yes flag to installer and update installation documentation
|
||||||
|
|
||||||
|
- Enhance install.sh to parse -y/--yes and -h/--help options, automating git installation when
|
||||||
|
auto-yes is provided
|
||||||
|
- Improve user prompts for dependency installation and provide clearer instructions
|
||||||
|
- Update readme.md to document new installer options and enhanced file system and service changes
|
||||||
|
details
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.3.0 - feat(installer/cli)
|
||||||
|
|
||||||
|
Add OS detection and git auto-installation support to install.sh and improve service setup prompt in
|
||||||
|
CLI
|
||||||
|
|
||||||
|
- Implemented helper functions in install.sh to detect OS type and automatically install git if
|
||||||
|
missing
|
||||||
|
- Prompt user for git installation if not present before cloning the repository
|
||||||
|
- Enhanced CLI service setup flow to offer starting the NUPST service immediately after installation
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.2.0 - feat(cli)
|
||||||
|
|
||||||
|
Add 'config' command to display current configuration and update CLI help
|
||||||
|
|
||||||
|
- Introduce new 'config' command to show SNMP settings, thresholds, and configuration file location
|
||||||
|
- Update help text to include details for 'nupst config' command
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.1.0 - feat(cli)
|
||||||
|
|
||||||
|
Add uninstall command to CLI and update shutdown delay for graceful VM shutdown
|
||||||
|
|
||||||
|
- Implement uninstall command in ts/cli.ts that locates and executes uninstall.sh with user prompts
|
||||||
|
- Update uninstall.sh to support environment variables for configuration and repository removal
|
||||||
|
- Increase shutdown delay in ts/snmp/manager.ts from 1 minute to 5 minutes to allow VMs more time to
|
||||||
|
shut down
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.0.1 - fix(cli/systemd)
|
||||||
|
|
||||||
|
Fix status command to pass debug flag and improve systemd status logging output
|
||||||
|
|
||||||
|
- ts/cli.ts: Now extracts debug options from process arguments and passes debug mode to getStatus.
|
||||||
|
- ts/systemd.ts: Updated getStatus to accept a debugMode parameter, enabling detailed SNMP debug
|
||||||
|
logging, explicitly reloading configuration, and printing connection details.
|
||||||
|
|
||||||
|
## 2025-03-25 - 2.0.0 - BREAKING CHANGE(snmp)
|
||||||
|
|
||||||
|
refactor: update SNMP type definitions and interface names for consistency
|
||||||
|
|
||||||
|
- Renamed SnmpConfig to ISnmpConfig, OIDSet to IOidSet, UpsStatus to IUpsStatus, and UpsModel to
|
||||||
|
TUpsModel in ts/snmp/types.ts.
|
||||||
|
- Updated internal references in ts/daemon.ts, ts/snmp/index.ts, ts/snmp/manager.ts,
|
||||||
|
ts/snmp/oid-sets.ts, ts/snmp/packet-creator.ts, and ts/snmp/packet-parser.ts to use the new
|
||||||
|
interface names.
|
||||||
|
|
||||||
|
## 2025-03-25 - 1.10.1 - fix(systemd/readme)
|
||||||
|
|
||||||
|
Improve README documentation and fix UPS status retrieval in systemd service
|
||||||
|
|
||||||
|
- Updated README features and installation instructions to clarify SNMP version support, UPS models,
|
||||||
|
and configuration
|
||||||
|
- Modified default SNMP host to '192.168.1.100' and added 'upsModel' property in configuration
|
||||||
|
examples
|
||||||
|
- Enhanced instructions for real-time log viewing and update process in README
|
||||||
|
- Fixed systemd.ts to use a test configuration with an appropriate timeout when fetching UPS status
|
||||||
|
|
||||||
|
## 2025-03-25 - 1.10.0 - feat(core)
|
||||||
|
|
||||||
|
Add update checking and version logging across startup components
|
||||||
|
|
||||||
|
- In daemon.ts, log version info on startup and check for updates in the background using npm
|
||||||
|
registry response
|
||||||
|
- In nupst.ts, implement getVersion, checkForUpdates, getUpdateStatus, and compareVersions functions
|
||||||
|
with update notifications
|
||||||
|
- Establish bidirectional reference between Nupst and NupstSnmp to support version logging
|
||||||
|
- Update systemd service status output to include version information
|
||||||
|
|
||||||
|
## 2025-03-25 - 1.9.0 - feat(cli)
|
||||||
|
|
||||||
|
Add update command to CLI to update NUPST from repository and refresh the systemd service
|
||||||
|
|
||||||
|
- Integrate 'update' subcommand in CLI command parser
|
||||||
|
- Update documentation and help output to include new command
|
||||||
|
- Implement update process that fetches changes from git, runs install.sh/setup.sh, and refreshes
|
||||||
|
systemd service if installed
|
||||||
|
|
||||||
|
## 2025-03-25 - 1.8.2 - fix(cli)
|
||||||
|
|
||||||
|
Refactor logs command to use child_process spawn for real-time log tailing
|
||||||
|
|
||||||
|
- Replaced execSync call with spawn to properly follow logs
|
||||||
|
- Forward SIGINT to the spawned process for graceful termination
|
||||||
|
- Await the child process exit to ensure clean shutdown of the CLI log command
|
||||||
|
|
||||||
|
## 2025-03-25 - 1.8.1 - fix(systemd)
|
||||||
|
|
||||||
|
Update ExecStart in systemd service template to use /opt/nupst/bin/nupst for daemon startup
|
||||||
|
|
||||||
|
- Changed ExecStart from '/usr/bin/nupst daemon-start' to '/opt/nupst/bin/nupst daemon-start' in the
|
||||||
|
systemd service file
|
||||||
|
- Ensures the service uses the correct binary installation path
|
||||||
|
|
||||||
## 2025-03-25 - 1.8.0 - feat(core)
|
## 2025-03-25 - 1.8.0 - feat(core)
|
||||||
|
|
||||||
Enhance SNMP module and interactive CLI setup for UPS shutdown
|
Enhance SNMP module and interactive CLI setup for UPS shutdown
|
||||||
|
|
||||||
- Refactored SNMP packet parsing and encoding utilities for clearer error handling and debugging
|
- Refactored SNMP packet parsing and encoding utilities for clearer error handling and debugging
|
||||||
@@ -9,22 +563,28 @@ Enhance SNMP module and interactive CLI setup for UPS shutdown
|
|||||||
- Expanded test coverage with simulated SNMP responses for various response types
|
- Expanded test coverage with simulated SNMP responses for various response types
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.6 - fix(core)
|
## 2025-03-25 - 1.7.6 - fix(core)
|
||||||
|
|
||||||
Refactor SNMP, systemd, and CLI modules to improve error handling, logging, and code clarity
|
Refactor SNMP, systemd, and CLI modules to improve error handling, logging, and code clarity
|
||||||
|
|
||||||
- Removed unused dependency 'net-snmp' from package.json
|
- Removed unused dependency 'net-snmp' from package.json
|
||||||
- Extracted helper functions for SNMP packet creation and parsing (using SnmpEncoder, SnmpPacketCreator and SnmpPacketParser)
|
- Extracted helper functions for SNMP packet creation and parsing (using SnmpEncoder,
|
||||||
- Improved debug logging and added detailed documentation comments across SNMP, systemd, CLI and daemon modules
|
SnmpPacketCreator and SnmpPacketParser)
|
||||||
|
- Improved debug logging and added detailed documentation comments across SNMP, systemd, CLI and
|
||||||
|
daemon modules
|
||||||
- Refactored systemd service management to extract status display and service disabling logic
|
- Refactored systemd service management to extract status display and service disabling logic
|
||||||
- Updated test suite to use proper modular methods from the new SNMP utilities
|
- Updated test suite to use proper modular methods from the new SNMP utilities
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.5 - fix(cli)
|
## 2025-03-25 - 1.7.5 - fix(cli)
|
||||||
Enable SNMP debug mode in CLI commands and update debug flag handling in daemon-start and test; bump version to 1.7.4
|
|
||||||
|
Enable SNMP debug mode in CLI commands and update debug flag handling in daemon-start and test; bump
|
||||||
|
version to 1.7.4
|
||||||
|
|
||||||
- Call enableDebug() on SNMP client earlier in command parsing
|
- Call enableDebug() on SNMP client earlier in command parsing
|
||||||
- Pass debug flag to 'daemon-start' and 'test' commands for consistent debug output
|
- Pass debug flag to 'daemon-start' and 'test' commands for consistent debug output
|
||||||
- Update package version from 1.7.3 to 1.7.4
|
- Update package version from 1.7.3 to 1.7.4
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.3 - fix(SNMP)
|
## 2025-03-25 - 1.7.3 - fix(SNMP)
|
||||||
|
|
||||||
Refine SNMP packet creation and response parsing for more reliable UPS status monitoring
|
Refine SNMP packet creation and response parsing for more reliable UPS status monitoring
|
||||||
|
|
||||||
- Improve error handling and fallback logic when parsing SNMP responses
|
- Improve error handling and fallback logic when parsing SNMP responses
|
||||||
@@ -32,13 +592,16 @@ Refine SNMP packet creation and response parsing for more reliable UPS status mo
|
|||||||
- Enhance test coverage for various UPS scenarios
|
- Enhance test coverage for various UPS scenarios
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.2 - fix(core)
|
## 2025-03-25 - 1.7.2 - fix(core)
|
||||||
Refactor internal SNMP response parsing and enhance daemon logging for improved error reporting and clarity.
|
|
||||||
|
Refactor internal SNMP response parsing and enhance daemon logging for improved error reporting and
|
||||||
|
clarity.
|
||||||
|
|
||||||
- Improved fallback and error handling in SNMP response parsing
|
- Improved fallback and error handling in SNMP response parsing
|
||||||
- Enhanced logging messages in daemon and systemd service management
|
- Enhanced logging messages in daemon and systemd service management
|
||||||
- Minor refactoring for better maintainability without functional changes
|
- Minor refactoring for better maintainability without functional changes
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.1 - fix(snmp-cli)
|
## 2025-03-25 - 1.7.1 - fix(snmp-cli)
|
||||||
|
|
||||||
Improve SNMP response parsing and CLI UPS connection timeout handling
|
Improve SNMP response parsing and CLI UPS connection timeout handling
|
||||||
|
|
||||||
- Expand parsing loop in SNMP responses to capture Gauge32 and Timeticks values
|
- Expand parsing loop in SNMP responses to capture Gauge32 and Timeticks values
|
||||||
@@ -46,14 +609,17 @@ Improve SNMP response parsing and CLI UPS connection timeout handling
|
|||||||
- Configure CLI test commands to use a shortened timeout for UPS connection tests
|
- Configure CLI test commands to use a shortened timeout for UPS connection tests
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.0 - feat(SNMP/UPS)
|
## 2025-03-25 - 1.7.0 - feat(SNMP/UPS)
|
||||||
|
|
||||||
Add UPS model selection and custom OIDs support to handle different UPS brands
|
Add UPS model selection and custom OIDs support to handle different UPS brands
|
||||||
|
|
||||||
- Introduce distinct OID sets for CyberPower, APC, Eaton, TrippLite, Liebert, and a custom option
|
- Introduce distinct OID sets for CyberPower, APC, Eaton, TrippLite, Liebert, and a custom option
|
||||||
- Update interactive setup to prompt for UPS model selection and custom OID entry when needed
|
- Update interactive setup to prompt for UPS model selection and custom OID entry when needed
|
||||||
- Refactor SNMP status retrieval to dynamically select the appropriate OIDs based on the configured UPS model
|
- Refactor SNMP status retrieval to dynamically select the appropriate OIDs based on the configured
|
||||||
|
UPS model
|
||||||
- Extend default configuration with an upsModel property for consistent behavior
|
- Extend default configuration with an upsModel property for consistent behavior
|
||||||
|
|
||||||
## 2025-03-25 - 1.6.0 - feat(cli,snmp)
|
## 2025-03-25 - 1.6.0 - feat(cli,snmp)
|
||||||
|
|
||||||
Enhance debug logging and add debug mode support in CLI and SNMP modules
|
Enhance debug logging and add debug mode support in CLI and SNMP modules
|
||||||
|
|
||||||
- Enable debug flags (--debug, -d) in CLI to trigger detailed SNMP logging
|
- Enable debug flags (--debug, -d) in CLI to trigger detailed SNMP logging
|
||||||
@@ -62,6 +628,7 @@ Enhance debug logging and add debug mode support in CLI and SNMP modules
|
|||||||
- Improve timeout and discovery logging details for streamlined troubleshooting
|
- Improve timeout and discovery logging details for streamlined troubleshooting
|
||||||
|
|
||||||
## 2025-03-25 - 1.5.0 - feat(cli)
|
## 2025-03-25 - 1.5.0 - feat(cli)
|
||||||
|
|
||||||
Enhance CLI output: display SNMPv3 auth/priv details and support timeout customization during setup
|
Enhance CLI output: display SNMPv3 auth/priv details and support timeout customization during setup
|
||||||
|
|
||||||
- Display authentication and privacy protocol details when SNMP version is 3
|
- Display authentication and privacy protocol details when SNMP version is 3
|
||||||
@@ -70,10 +637,11 @@ Enhance CLI output: display SNMPv3 auth/priv details and support timeout customi
|
|||||||
- Allow users to customize SNMP timeout during interactive setup
|
- Allow users to customize SNMP timeout during interactive setup
|
||||||
|
|
||||||
## 2025-03-25 - 1.4.1 - fix(version)
|
## 2025-03-25 - 1.4.1 - fix(version)
|
||||||
|
|
||||||
Bump patch version for consistency with commit info
|
Bump patch version for consistency with commit info
|
||||||
|
|
||||||
|
|
||||||
## 2025-03-25 - 1.4.0 - feat(snmp)
|
## 2025-03-25 - 1.4.0 - feat(snmp)
|
||||||
|
|
||||||
Implement native SNMPv3 support with simulated encryption and enhanced authentication handling.
|
Implement native SNMPv3 support with simulated encryption and enhanced authentication handling.
|
||||||
|
|
||||||
- Add fully native SNMPv3 GET request implementation replacing the snmpwalk fallback
|
- Add fully native SNMPv3 GET request implementation replacing the snmpwalk fallback
|
||||||
@@ -82,12 +650,14 @@ Implement native SNMPv3 support with simulated encryption and enhanced authentic
|
|||||||
- Introduce detailed security parameter management for SNMPv3
|
- Introduce detailed security parameter management for SNMPv3
|
||||||
|
|
||||||
## 2025-03-25 - 1.3.1 - fix(cli)
|
## 2025-03-25 - 1.3.1 - fix(cli)
|
||||||
|
|
||||||
Remove redundant SNMP tools checks in CLI and Systemd modules
|
Remove redundant SNMP tools checks in CLI and Systemd modules
|
||||||
|
|
||||||
- Eliminate unnecessary snmpwalk dependency checks in the test command and interactive setup flow.
|
- Eliminate unnecessary snmpwalk dependency checks in the test command and interactive setup flow.
|
||||||
- Adjust systemd configuration file check to avoid external dependency verification.
|
- Adjust systemd configuration file check to avoid external dependency verification.
|
||||||
|
|
||||||
## 2025-03-25 - 1.3.0 - feat(cli)
|
## 2025-03-25 - 1.3.0 - feat(cli)
|
||||||
|
|
||||||
add test command to verify UPS SNMP configuration and connectivity
|
add test command to verify UPS SNMP configuration and connectivity
|
||||||
|
|
||||||
- Introduce a new 'test' command in the CLI to check the SNMP configuration and UPS connection.
|
- Introduce a new 'test' command in the CLI to check the SNMP configuration and UPS connection.
|
||||||
@@ -95,6 +665,7 @@ add test command to verify UPS SNMP configuration and connectivity
|
|||||||
- Output UPS status details and compare against defined shutdown thresholds.
|
- Output UPS status details and compare against defined shutdown thresholds.
|
||||||
|
|
||||||
## 2025-03-25 - 1.2.6 - fix(cli)
|
## 2025-03-25 - 1.2.6 - fix(cli)
|
||||||
|
|
||||||
Refactor interactive setup to use dynamic import for readline and ensure proper cleanup
|
Refactor interactive setup to use dynamic import for readline and ensure proper cleanup
|
||||||
|
|
||||||
- Replaced synchronous require() with async import for ESM compatibility
|
- Replaced synchronous require() with async import for ESM compatibility
|
||||||
@@ -102,13 +673,16 @@ Refactor interactive setup to use dynamic import for readline and ensure proper
|
|||||||
- Enhanced error logging by outputting error.message
|
- Enhanced error logging by outputting error.message
|
||||||
|
|
||||||
## 2025-03-25 - 1.2.5 - fix(error-handling)
|
## 2025-03-25 - 1.2.5 - fix(error-handling)
|
||||||
Improve error handling in CLI, daemon, and systemd lifecycle management with enhanced logging for configuration issues
|
|
||||||
|
Improve error handling in CLI, daemon, and systemd lifecycle management with enhanced logging for
|
||||||
|
configuration issues
|
||||||
|
|
||||||
- Wrap daemon and service start commands in try-catch blocks to properly handle and log errors
|
- Wrap daemon and service start commands in try-catch blocks to properly handle and log errors
|
||||||
- Throw explicit errors when configuration file is missing instead of silently defaulting
|
- Throw explicit errors when configuration file is missing instead of silently defaulting
|
||||||
- Enhance log messages for service installation, startup, and status retrieval for clearer debugging
|
- Enhance log messages for service installation, startup, and status retrieval for clearer debugging
|
||||||
|
|
||||||
## 2025-03-25 - 1.2.4 - fix(cli/daemon)
|
## 2025-03-25 - 1.2.4 - fix(cli/daemon)
|
||||||
|
|
||||||
Improve logging and user feedback in interactive setup and UPS monitoring
|
Improve logging and user feedback in interactive setup and UPS monitoring
|
||||||
|
|
||||||
- Refactor configuration summary output in the interactive setup for clearer display
|
- Refactor configuration summary output in the interactive setup for clearer display
|
||||||
@@ -116,17 +690,20 @@ Improve logging and user feedback in interactive setup and UPS monitoring
|
|||||||
- Improve error messages and user guidance during configuration and monitoring
|
- Improve error messages and user guidance during configuration and monitoring
|
||||||
|
|
||||||
## 2025-03-24 - 1.2.3 - fix(nupst)
|
## 2025-03-24 - 1.2.3 - fix(nupst)
|
||||||
|
|
||||||
No changes
|
No changes
|
||||||
|
|
||||||
|
|
||||||
## 2025-03-24 - 1.2.2 - fix(bin/nupst)
|
## 2025-03-24 - 1.2.2 - fix(bin/nupst)
|
||||||
Improve symlink resolution in launcher script to correctly determine project root based on execution path.
|
|
||||||
|
Improve symlink resolution in launcher script to correctly determine project root based on execution
|
||||||
|
path.
|
||||||
|
|
||||||
- Replace directory determination with readlink for accurate symlink resolution
|
- Replace directory determination with readlink for accurate symlink resolution
|
||||||
- Set project root to '/opt/nupst' when script is run via symlink from /usr/local/bin
|
- Set project root to '/opt/nupst' when script is run via symlink from /usr/local/bin
|
||||||
- Add debugging comments to assist with path resolution
|
- Add debugging comments to assist with path resolution
|
||||||
|
|
||||||
## 2025-03-24 - 1.2.1 - fix(bin)
|
## 2025-03-24 - 1.2.1 - fix(bin)
|
||||||
|
|
||||||
Simplify Node.js binary detection in installation script
|
Simplify Node.js binary detection in installation script
|
||||||
|
|
||||||
- Directly set Node binary path to vendor/node-linux-x64/bin/node
|
- Directly set Node binary path to vendor/node-linux-x64/bin/node
|
||||||
@@ -134,59 +711,78 @@ Simplify Node.js binary detection in installation script
|
|||||||
- Fallback to system Node if vendor binary is not found
|
- Fallback to system Node if vendor binary is not found
|
||||||
|
|
||||||
## 2025-03-24 - 1.2.0 - feat(installer)
|
## 2025-03-24 - 1.2.0 - feat(installer)
|
||||||
|
|
||||||
Improve Node.js binary detection and dynamic LTS version retrieval in setup scripts
|
Improve Node.js binary detection and dynamic LTS version retrieval in setup scripts
|
||||||
|
|
||||||
- Enhanced bin/nupst to search multiple possible locations for the Node.js binary and fallback to system node if necessary
|
- Enhanced bin/nupst to search multiple possible locations for the Node.js binary and fallback to
|
||||||
- Updated setup.sh to fetch the latest LTS Node.js version from nodejs.org and use a fallback version when the request fails
|
system node if necessary
|
||||||
|
- Updated setup.sh to fetch the latest LTS Node.js version from nodejs.org and use a fallback
|
||||||
|
version when the request fails
|
||||||
|
|
||||||
## 2025-03-24 - 1.1.2 - fix(setup.sh)
|
## 2025-03-24 - 1.1.2 - fix(setup.sh)
|
||||||
Improve error handling in setup.sh: exit immediately when the downloaded npm package lacks the dist_ts directory, removing the fallback build-from-source mechanism.
|
|
||||||
|
Improve error handling in setup.sh: exit immediately when the downloaded npm package lacks the
|
||||||
|
dist_ts directory, removing the fallback build-from-source mechanism.
|
||||||
|
|
||||||
- Removed BUILD_FROM_SOURCE logic that attempted to build from source on missing dist_ts directory
|
- Removed BUILD_FROM_SOURCE logic that attempted to build from source on missing dist_ts directory
|
||||||
- Updated error messages to clearly indicate failure in downloading a valid package
|
- Updated error messages to clearly indicate failure in downloading a valid package
|
||||||
- Ensured installation halts if essential files are missing
|
- Ensured installation halts if essential files are missing
|
||||||
|
|
||||||
## 2025-03-24 - 1.1.1 - fix(package.json)
|
## 2025-03-24 - 1.1.1 - fix(package.json)
|
||||||
|
|
||||||
Remove unused prepublishOnly script and update files field in package.json
|
Remove unused prepublishOnly script and update files field in package.json
|
||||||
|
|
||||||
- Removed prepublishOnly build trigger
|
- Removed prepublishOnly build trigger
|
||||||
- Updated files list to accurately include intended directories and files
|
- Updated files list to accurately include intended directories and files
|
||||||
|
|
||||||
## 2025-03-24 - 1.1.0 - feat(installer-setup)
|
## 2025-03-24 - 1.1.0 - feat(installer-setup)
|
||||||
|
|
||||||
Enhance installer and setup scripts for improved global installation and artifact management
|
Enhance installer and setup scripts for improved global installation and artifact management
|
||||||
|
|
||||||
- Detect piped installation in install.sh, clone repository automatically, and clean up previous installations
|
- Detect piped installation in install.sh, clone repository automatically, and clean up previous
|
||||||
|
installations
|
||||||
- Update readme.md with correct repository URL and clearer installation instructions
|
- Update readme.md with correct repository URL and clearer installation instructions
|
||||||
- Improve setup.sh to remove existing dist_ts, download build artifacts from the npm registry, and simplify dependency installation
|
- Improve setup.sh to remove existing dist_ts, download build artifacts from the npm registry, and
|
||||||
|
simplify dependency installation
|
||||||
|
|
||||||
## 2025-03-24 - 1.0.1 - fix(version)
|
## 2025-03-24 - 1.0.1 - fix(version)
|
||||||
|
|
||||||
Bump version to 1.0.1
|
Bump version to 1.0.1
|
||||||
|
|
||||||
- Updated commitinfo data to reflect the new patch version.
|
- Updated commitinfo data to reflect the new patch version.
|
||||||
- Synchronized version information between commitinfo file and package metadata.
|
- Synchronized version information between commitinfo file and package metadata.
|
||||||
|
|
||||||
## 2025-03-24 - 1.0.1 - fix(build)
|
## 2025-03-24 - 1.0.1 - fix(build)
|
||||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and adjust distribution paths in .gitignore
|
|
||||||
|
Update build script to use 'tsbuild tsfolders --allowimplicitany' and adjust distribution paths in
|
||||||
|
.gitignore
|
||||||
|
|
||||||
- Replaced 'tsc' with 'tsbuild tsfolders --allowimplicitany' in package.json
|
- Replaced 'tsc' with 'tsbuild tsfolders --allowimplicitany' in package.json
|
||||||
- Updated .gitignore to reflect new compiled distribution folder pattern
|
- Updated .gitignore to reflect new compiled distribution folder pattern
|
||||||
- Updated changelog to document build improvements and regenerated type definitions
|
- Updated changelog to document build improvements and regenerated type definitions
|
||||||
|
|
||||||
## 2025-03-24 - 1.0.1 - fix(build)
|
## 2025-03-24 - 1.0.1 - fix(build)
|
||||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type definitions for CLI, daemon, index, nupst, snmp, and systemd modules
|
|
||||||
|
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type
|
||||||
|
definitions for CLI, daemon, index, nupst, snmp, and systemd modules
|
||||||
|
|
||||||
- Replaced 'tsc' command with tsbuild in package.json
|
- Replaced 'tsc' command with tsbuild in package.json
|
||||||
- Updated .gitignore to reflect new compiled distribution folder pattern
|
- Updated .gitignore to reflect new compiled distribution folder pattern
|
||||||
- Added new dist_ts files including .d.ts type definitions and compiled JavaScript for multiple modules
|
- Added new dist_ts files including .d.ts type definitions and compiled JavaScript for multiple
|
||||||
|
modules
|
||||||
|
|
||||||
## 2025-03-24 - 1.0.1 - fix(build)
|
## 2025-03-24 - 1.0.1 - fix(build)
|
||||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type definitions for CLI, daemon, nupst, snmp, and systemd modules.
|
|
||||||
|
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type
|
||||||
|
definitions for CLI, daemon, nupst, snmp, and systemd modules.
|
||||||
|
|
||||||
- Replaced the 'tsc' command with 'tsbuild tsfolders --allowimplicitany' in package.json.
|
- Replaced the 'tsc' command with 'tsbuild tsfolders --allowimplicitany' in package.json.
|
||||||
- Added new dist_ts files including type definitions (d.ts) and compiled JavaScript for CLI, daemon, index, nupst, snmp, and systemd.
|
- Added new dist_ts files including type definitions (d.ts) and compiled JavaScript for CLI, daemon,
|
||||||
|
index, nupst, snmp, and systemd.
|
||||||
- Improved the generated CLI declarations and overall distribution build.
|
- Improved the generated CLI declarations and overall distribution build.
|
||||||
|
|
||||||
## 2025-03-23 - 1.0.0 - initial setup
|
## 2025-03-23 - 1.0.0 - initial setup
|
||||||
|
|
||||||
This range covers the early commits that mainly established the repository structure.
|
This range covers the early commits that mainly established the repository structure.
|
||||||
|
|
||||||
- Initial repository commit with basic project initialization.
|
- Initial repository commit with basic project initialization.
|
||||||
|
36
deno.json
Normal file
36
deno.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "@serve.zone/nupst",
|
||||||
|
"version": "4.1.4",
|
||||||
|
"exports": "./mod.ts",
|
||||||
|
"tasks": {
|
||||||
|
"dev": "deno run --allow-all mod.ts",
|
||||||
|
"compile": "deno task compile:all",
|
||||||
|
"compile:all": "bash scripts/compile-all.sh",
|
||||||
|
"test": "deno test --allow-all test/",
|
||||||
|
"test:watch": "deno test --allow-all --watch test/",
|
||||||
|
"check": "deno check mod.ts",
|
||||||
|
"fmt": "deno fmt",
|
||||||
|
"lint": "deno lint"
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"rules": {
|
||||||
|
"tags": ["recommended"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fmt": {
|
||||||
|
"useTabs": false,
|
||||||
|
"lineWidth": 100,
|
||||||
|
"indentWidth": 2,
|
||||||
|
"semiColons": true,
|
||||||
|
"singleQuote": true
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["deno.window"],
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"@std/cli": "jsr:@std/cli@^1.0.0",
|
||||||
|
"@std/fmt": "jsr:@std/fmt@^1.0.0",
|
||||||
|
"@std/path": "jsr:@std/path@^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
377
install.sh
377
install.sh
@@ -1,8 +1,71 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# NUPST Installer Script
|
# NUPST Installer Script (v4.0+)
|
||||||
# Downloads and installs NUPST globally on the system
|
# Downloads and installs pre-compiled NUPST binary from Gitea releases
|
||||||
# Can be used directly with curl: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
#
|
||||||
|
# Usage:
|
||||||
|
# Direct piped installation (recommended):
|
||||||
|
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
|
#
|
||||||
|
# With version specification:
|
||||||
|
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -h, --help Show this help message
|
||||||
|
# --version VERSION Install specific version (e.g., v4.0.0)
|
||||||
|
# --install-dir DIR Installation directory (default: /opt/nupst)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
SHOW_HELP=0
|
||||||
|
SPECIFIED_VERSION=""
|
||||||
|
INSTALL_DIR="/opt/nupst"
|
||||||
|
GITEA_BASE_URL="https://code.foss.global"
|
||||||
|
GITEA_REPO="serve.zone/nupst"
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-h|--help)
|
||||||
|
SHOW_HELP=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--version)
|
||||||
|
SPECIFIED_VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--install-dir)
|
||||||
|
INSTALL_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
echo "Use -h or --help for usage information"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $SHOW_HELP -eq 1 ]; then
|
||||||
|
echo "NUPST Installer Script (v4.0+)"
|
||||||
|
echo "Downloads and installs pre-compiled NUPST binary"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: $0 [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -h, --help Show this help message"
|
||||||
|
echo " --version VERSION Install specific version (e.g., v4.0.0)"
|
||||||
|
echo " --install-dir DIR Installation directory (default: /opt/nupst)"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " # Install latest version"
|
||||||
|
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
|
||||||
|
echo ""
|
||||||
|
echo " # Install specific version"
|
||||||
|
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if running as root
|
# Check if running as root
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
@@ -10,88 +73,248 @@ if [ "$EUID" -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Detect if script is being piped or run directly
|
# Helper function to detect OS and architecture
|
||||||
PIPED=0
|
detect_platform() {
|
||||||
if [ ! -t 0 ]; then
|
local os=$(uname -s)
|
||||||
# Being piped, need to clone the repo
|
local arch=$(uname -m)
|
||||||
PIPED=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Define installation directory
|
# Map OS
|
||||||
INSTALL_DIR="/opt/nupst"
|
case "$os" in
|
||||||
REPO_URL="https://code.foss.global/serve.zone/nupst.git"
|
Linux)
|
||||||
|
os_name="linux"
|
||||||
|
;;
|
||||||
|
Darwin)
|
||||||
|
os_name="macos"
|
||||||
|
;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
os_name="windows"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: Unsupported operating system: $os"
|
||||||
|
echo "Supported: Linux, macOS, Windows"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if [ $PIPED -eq 1 ]; then
|
# Map architecture
|
||||||
echo "Installing NUPST from remote repository..."
|
case "$arch" in
|
||||||
|
x86_64|amd64)
|
||||||
# Check if git is installed
|
arch_name="x64"
|
||||||
if ! command -v git &> /dev/null; then
|
;;
|
||||||
echo "Git is required but not installed. Please install git first."
|
aarch64|arm64)
|
||||||
exit 1
|
arch_name="arm64"
|
||||||
fi
|
;;
|
||||||
|
*)
|
||||||
# Check if installation directory exists
|
echo "Error: Unsupported architecture: $arch"
|
||||||
if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then
|
echo "Supported: x86_64/amd64 (x64), aarch64/arm64 (arm64)"
|
||||||
echo "Existing installation found at $INSTALL_DIR. Updating..."
|
exit 1
|
||||||
cd "$INSTALL_DIR"
|
;;
|
||||||
|
esac
|
||||||
# Try to update the repository
|
|
||||||
git fetch origin
|
# Construct binary name
|
||||||
git reset --hard origin/main
|
if [ "$os_name" = "windows" ]; then
|
||||||
|
echo "nupst-${os_name}-${arch_name}.exe"
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Failed to update repository. Reinstalling..."
|
|
||||||
cd /
|
|
||||||
rm -rf "$INSTALL_DIR"
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
|
||||||
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
|
|
||||||
else
|
|
||||||
echo "Repository updated successfully."
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
# Fresh installation
|
echo "nupst-${os_name}-${arch_name}"
|
||||||
if [ -d "$INSTALL_DIR" ]; then
|
|
||||||
echo "Removing previous installation at $INSTALL_DIR..."
|
|
||||||
rm -rf "$INSTALL_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create installation directory
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
|
||||||
|
|
||||||
# Clone the repository
|
|
||||||
echo "Cloning NUPST repository to $INSTALL_DIR..."
|
|
||||||
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
|
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Failed to clone/update repository. Please check your internet connection."
|
# Get latest release version from Gitea API
|
||||||
|
get_latest_version() {
|
||||||
|
echo "Fetching latest release version from Gitea..." >&2
|
||||||
|
|
||||||
|
local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest"
|
||||||
|
local response=$(curl -sSL "$api_url" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ $? -ne 0 ] || [ -z "$response" ]; then
|
||||||
|
echo "Error: Failed to fetch latest release information from Gitea API" >&2
|
||||||
|
echo "URL: $api_url" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set script directory to the cloned repo
|
|
||||||
SCRIPT_DIR="$INSTALL_DIR"
|
|
||||||
else
|
|
||||||
# Running directly from within the repo
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run setup script
|
# Extract tag_name from JSON response
|
||||||
echo "Running setup script..."
|
local version=$(echo "$response" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
|
||||||
bash "$SCRIPT_DIR/setup.sh"
|
|
||||||
|
|
||||||
# Install globally
|
if [ -z "$version" ]; then
|
||||||
echo "Installing NUPST globally..."
|
echo "Error: Could not determine latest version from API response" >&2
|
||||||
ln -sf "$SCRIPT_DIR/bin/nupst" /usr/local/bin/nupst
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Installation completed
|
echo "$version"
|
||||||
if [ $PIPED -eq 1 ]; then
|
}
|
||||||
echo "NUPST has been installed globally at $INSTALL_DIR"
|
|
||||||
else
|
|
||||||
echo "NUPST has been installed globally."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "You can now run 'nupst' from anywhere."
|
# Main installation process
|
||||||
|
echo "================================================"
|
||||||
|
echo " NUPST Installation Script (v4.0+)"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Detect platform
|
||||||
|
BINARY_NAME=$(detect_platform)
|
||||||
|
echo "Detected platform: $BINARY_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Determine version to install
|
||||||
|
if [ -n "$SPECIFIED_VERSION" ]; then
|
||||||
|
VERSION="$SPECIFIED_VERSION"
|
||||||
|
echo "Installing specified version: $VERSION"
|
||||||
|
else
|
||||||
|
VERSION=$(get_latest_version)
|
||||||
|
echo "Installing latest version: $VERSION"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Construct download URL
|
||||||
|
DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BINARY_NAME}"
|
||||||
|
echo "Download URL: $DOWNLOAD_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if installation directory exists
|
||||||
|
SERVICE_WAS_RUNNING=0
|
||||||
|
OLD_NODE_INSTALL=0
|
||||||
|
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
# Check if this is an old Node.js-based installation
|
||||||
|
if [ -f "$INSTALL_DIR/package.json" ] || [ -d "$INSTALL_DIR/node_modules" ]; then
|
||||||
|
OLD_NODE_INSTALL=1
|
||||||
|
echo "Detected old Node.js-based NUPST installation (v3.x or earlier)"
|
||||||
|
echo "This installer will migrate to the new Deno-based binary version (v4.0+)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Updating existing installation at $INSTALL_DIR..."
|
||||||
|
|
||||||
|
# Check if service exists (enabled or running) and stop it if active
|
||||||
|
if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then
|
||||||
|
SERVICE_WAS_RUNNING=1
|
||||||
|
if systemctl is-active --quiet nupst 2>/dev/null; then
|
||||||
|
echo "Stopping NUPST service..."
|
||||||
|
systemctl stop nupst
|
||||||
|
else
|
||||||
|
echo "Service is installed but not currently running (will be updated)..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up old Node.js installation files
|
||||||
|
if [ $OLD_NODE_INSTALL -eq 1 ]; then
|
||||||
|
echo "Cleaning up old Node.js installation files..."
|
||||||
|
rm -rf "$INSTALL_DIR/node_modules" 2>/dev/null || true
|
||||||
|
rm -rf "$INSTALL_DIR/vendor" 2>/dev/null || true
|
||||||
|
rm -rf "$INSTALL_DIR/dist_ts" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/package.json" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/package-lock.json" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/pnpm-lock.yaml" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/tsconfig.json" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/setup.sh" 2>/dev/null || true
|
||||||
|
rm -rf "$INSTALL_DIR/bin" 2>/dev/null || true
|
||||||
|
echo "Old installation files removed."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Creating installation directory: $INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download binary
|
||||||
|
echo "Downloading NUPST binary..."
|
||||||
|
TEMP_FILE="$INSTALL_DIR/nupst.download"
|
||||||
|
curl -sSL "$DOWNLOAD_URL" -o "$TEMP_FILE"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to download binary from $DOWNLOAD_URL"
|
||||||
|
echo ""
|
||||||
|
echo "Please check:"
|
||||||
|
echo " 1. Your internet connection"
|
||||||
|
echo " 2. The specified version exists: ${GITEA_BASE_URL}/${GITEA_REPO}/releases"
|
||||||
|
echo " 3. The platform binary is available for this release"
|
||||||
|
rm -f "$TEMP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if download was successful (file exists and not empty)
|
||||||
|
if [ ! -s "$TEMP_FILE" ]; then
|
||||||
|
echo "Error: Downloaded file is empty or does not exist"
|
||||||
|
rm -f "$TEMP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Move to final location
|
||||||
|
BINARY_PATH="$INSTALL_DIR/nupst"
|
||||||
|
mv "$TEMP_FILE" "$BINARY_PATH"
|
||||||
|
|
||||||
|
# Make executable
|
||||||
|
chmod +x "$BINARY_PATH"
|
||||||
|
|
||||||
|
echo "Binary installed successfully to: $BINARY_PATH"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if /usr/local/bin is in PATH
|
||||||
|
if [[ ":$PATH:" == *":/usr/local/bin:"* ]]; then
|
||||||
|
BIN_DIR="/usr/local/bin"
|
||||||
|
else
|
||||||
|
BIN_DIR="/usr/bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create symlink for global access
|
||||||
|
ln -sf "$BINARY_PATH" "$BIN_DIR/nupst"
|
||||||
|
echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Update systemd service file if migrating from v3
|
||||||
|
if [ $SERVICE_WAS_RUNNING -eq 1 ] && [ $OLD_NODE_INSTALL -eq 1 ]; then
|
||||||
|
echo "Updating systemd service file for v4..."
|
||||||
|
$BINARY_PATH service enable > /dev/null 2>&1
|
||||||
|
echo "Service file updated."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restart service if it was running before update
|
||||||
|
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
|
||||||
|
echo "Restarting NUPST service..."
|
||||||
|
systemctl start nupst
|
||||||
|
echo "Service restarted successfully."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "================================================"
|
||||||
|
echo " NUPST Installation Complete!"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $OLD_NODE_INSTALL -eq 1 ]; then
|
||||||
|
echo "Migration from v3.x to v4.0 successful!"
|
||||||
|
echo ""
|
||||||
|
echo "What changed:"
|
||||||
|
echo " • Node.js runtime removed (now a self-contained binary)"
|
||||||
|
echo " • Faster startup and lower memory usage"
|
||||||
|
echo " • CLI commands now use subcommand structure"
|
||||||
|
echo " (old commands still work with deprecation warnings)"
|
||||||
|
echo ""
|
||||||
|
echo "See readme for migration details: https://code.foss.global/serve.zone/nupst#migration-from-v3x"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installation details:"
|
||||||
|
echo " Binary location: $BINARY_PATH"
|
||||||
|
echo " Symlink location: $BIN_DIR/nupst"
|
||||||
|
echo " Version: $VERSION"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if configuration exists
|
||||||
|
if [ -f "/etc/nupst/config.json" ]; then
|
||||||
|
echo "Configuration: /etc/nupst/config.json (preserved)"
|
||||||
|
echo ""
|
||||||
|
echo "Your existing configuration has been preserved."
|
||||||
|
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
|
||||||
|
echo "The service has been restarted with your current settings."
|
||||||
|
else
|
||||||
|
echo "Start the service with: sudo nupst service start"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Get started:"
|
||||||
|
echo " nupst --version"
|
||||||
|
echo " nupst help"
|
||||||
|
echo " nupst ups add # Add a UPS device"
|
||||||
|
echo " nupst service enable # Enable systemd service"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
echo "To get started, try:"
|
|
||||||
echo " nupst help"
|
|
||||||
echo " nupst setup # To configure your UPS connection"
|
|
||||||
|
21
license
Normal file
21
license
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 Task Venture Capital GmbH
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
44
mod.ts
Normal file
44
mod.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env -S deno run --allow-all
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NUPST - UPS Shutdown Tool
|
||||||
|
*
|
||||||
|
* A command-line tool for monitoring SNMP-enabled UPS devices and
|
||||||
|
* initiating system shutdown when power conditions are critical.
|
||||||
|
*
|
||||||
|
* Required Permissions:
|
||||||
|
* - --allow-net: SNMP communication with UPS devices
|
||||||
|
* - --allow-read: Read configuration files (/etc/nupst/config.json)
|
||||||
|
* - --allow-write: Write configuration files
|
||||||
|
* - --allow-run: Execute system commands (systemctl, shutdown, git, bash)
|
||||||
|
* - --allow-sys: Access system information (hostname, OS details)
|
||||||
|
* - --allow-env: Read environment variables
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NupstCli } from './ts/cli.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for the NUPST application
|
||||||
|
* Parses command-line arguments and executes the requested command
|
||||||
|
*/
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const cli = new NupstCli();
|
||||||
|
|
||||||
|
// Deno.args is already 0-indexed (unlike Node's process.argv which starts at index 2)
|
||||||
|
// We need to prepend placeholder args to match the existing CLI parser expectations
|
||||||
|
const args = ['deno', 'mod.ts', ...Deno.args];
|
||||||
|
|
||||||
|
await cli.parseAndExecute(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute main and handle errors
|
||||||
|
if (import.meta.main) {
|
||||||
|
try {
|
||||||
|
await main();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
}
|
@@ -1 +0,0 @@
|
|||||||
{}
|
|
58
package.json
58
package.json
@@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@serve.zone/nupst",
|
|
||||||
"version": "1.8.0",
|
|
||||||
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"bin": {
|
|
||||||
"nupst": "bin/nupst"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsbuild tsfolders --allowimplicitany",
|
|
||||||
"start": "bin/nupst",
|
|
||||||
"setup": "bash setup.sh",
|
|
||||||
"test": "tstest test/",
|
|
||||||
"install-global": "sudo bash install.sh",
|
|
||||||
"uninstall": "sudo bash uninstall.sh"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"ups",
|
|
||||||
"snmp",
|
|
||||||
"shutdown",
|
|
||||||
"node",
|
|
||||||
"cli"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
"ts/**/*",
|
|
||||||
"ts_web/**/*",
|
|
||||||
"dist/**/*",
|
|
||||||
"dist_*/**/*",
|
|
||||||
"dist_ts/**/*",
|
|
||||||
"dist_ts_web/**/*",
|
|
||||||
"assets/**/*",
|
|
||||||
"cli.js",
|
|
||||||
"npmextra.json",
|
|
||||||
"readme.md"
|
|
||||||
],
|
|
||||||
"author": "",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
|
||||||
"@git.zone/tsbuild": "^2.3.2",
|
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
|
||||||
"@git.zone/tstest": "^1.0.96",
|
|
||||||
"@push.rocks/qenv": "^6.1.0",
|
|
||||||
"@push.rocks/tapbundle": "^5.6.0",
|
|
||||||
"@types/node": "^20.11.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.0.0"
|
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"esbuild",
|
|
||||||
"mongodb-memory-server",
|
|
||||||
"puppeteer"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
10187
pnpm-lock.yaml
generated
10187
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
686
readme.md
686
readme.md
@@ -1,146 +1,658 @@
|
|||||||
# NUPST - Node.js UPS Shutdown Tool
|
# NUPST - Network UPS Shutdown Tool
|
||||||
|
|
||||||
NUPST is a command-line tool that monitors SNMP-enabled UPS devices and initiates system shutdown when power outages are detected and battery levels are low.
|
NUPST is a lightweight, self-contained command-line tool that monitors SNMP-enabled UPS devices and
|
||||||
|
initiates system shutdown when power outages are detected and battery levels are low.
|
||||||
|
|
||||||
|
**Version 4.0+** is powered by Deno and distributed as pre-compiled binaries requiring zero
|
||||||
|
dependencies.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Monitors UPS devices using SNMP
|
- **Multi-UPS Support**: Monitor and manage multiple UPS devices from a single installation
|
||||||
- Automatic shutdown when battery level falls below threshold
|
- **Group Management**: Organize UPS devices into groups with different operating modes
|
||||||
- Automatic shutdown when runtime remaining falls below threshold
|
- **Redundant Mode**: Only shutdown when ALL UPS devices in a group are in critical condition
|
||||||
- Simple systemd service integration
|
- **Non-Redundant Mode**: Shutdown when ANY UPS device in a group is in critical condition
|
||||||
- Self-contained - includes its own Node.js runtime
|
- **SNMP Protocol Support**: Full support for SNMP v1, v2c, and v3 with authentication and
|
||||||
|
encryption
|
||||||
|
- **Multiple UPS Brands**: Works with CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv, and custom
|
||||||
|
OID configurations
|
||||||
|
- **Systemd Integration**: Simple service installation and management
|
||||||
|
- **Real-time Monitoring**: Live status updates and log viewing
|
||||||
|
- **Zero Dependencies**: Single self-contained binary with no runtime requirements
|
||||||
|
- **Cross-Platform**: Binaries available for Linux (x64, ARM64), macOS (Intel, Apple Silicon), and
|
||||||
|
Windows
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Quick Install (One-line command)
|
### Quick Install (Recommended)
|
||||||
|
|
||||||
|
The easiest way to install NUPST is using the automated installer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install directly without cloning the repository (requires root privileges)
|
# One-line installation
|
||||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
```
|
```
|
||||||
|
|
||||||
### Direct from Git
|
The installer will:
|
||||||
|
|
||||||
|
1. Auto-detect your platform (OS and architecture)
|
||||||
|
2. Download the latest pre-compiled binary from releases
|
||||||
|
3. Install to `/opt/nupst/nupst`
|
||||||
|
4. Create a symlink in `/usr/local/bin/nupst` for global access
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
Download the appropriate binary for your platform from the
|
||||||
|
[releases page](https://code.foss.global/serve.zone/nupst/releases):
|
||||||
|
|
||||||
|
- **Linux x64**: `nupst-linux-x64`
|
||||||
|
- **Linux ARM64**: `nupst-linux-arm64`
|
||||||
|
- **macOS Intel**: `nupst-macos-x64`
|
||||||
|
- **macOS Apple Silicon**: `nupst-macos-arm64`
|
||||||
|
- **Windows x64**: `nupst-windows-x64.exe`
|
||||||
|
|
||||||
|
Then install manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Download binary (replace with your platform)
|
||||||
git clone https://code.foss.global/serve.zone/nupst.git
|
curl -sSL https://code.foss.global/serve.zone/nupst/releases/download/v4.0.0/nupst-linux-x64 -o nupst
|
||||||
cd nupst
|
|
||||||
|
|
||||||
# Option 1: Quick install (requires root privileges)
|
# Make executable
|
||||||
sudo ./install.sh
|
chmod +x nupst
|
||||||
|
|
||||||
# Option 2: Manual setup
|
# Move to system path
|
||||||
./setup.sh
|
sudo mv nupst /usr/local/bin/nupst
|
||||||
sudo ln -s $(pwd)/bin/nupst /usr/local/bin/nupst
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### From NPM
|
### Installation Options
|
||||||
|
|
||||||
|
The installer script (`install.sh`) supports the following options:
|
||||||
|
|
||||||
|
```
|
||||||
|
-h, --help Show help message
|
||||||
|
--version VERSION Install specific version (e.g., --version v4.0.0)
|
||||||
|
--install-dir DIR Custom installation directory (default: /opt/nupst)
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @serve.zone/nupst
|
# Install specific version
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0
|
||||||
|
|
||||||
|
# Custom installation directory
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --install-dir /usr/local/nupst
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## System Changes
|
||||||
|
|
||||||
|
When installed, NUPST makes the following changes to your system:
|
||||||
|
|
||||||
|
### File System Changes
|
||||||
|
|
||||||
|
| Path | Description |
|
||||||
|
| ----------------------------------- | -------------------------------------- |
|
||||||
|
| `/opt/nupst/nupst` | Pre-compiled binary (default location) |
|
||||||
|
| `/etc/nupst/config.json` | Configuration file |
|
||||||
|
| `/usr/local/bin/nupst` | Symlink to the NUPST binary |
|
||||||
|
| `/etc/systemd/system/nupst.service` | Systemd service file (when enabled) |
|
||||||
|
|
||||||
|
### Service Changes
|
||||||
|
|
||||||
|
- Creates and enables a systemd service called `nupst.service` (when enabled with
|
||||||
|
`nupst service enable`)
|
||||||
|
- The service runs with root permissions to allow system shutdown capabilities
|
||||||
|
|
||||||
|
### Network Access
|
||||||
|
|
||||||
|
- NUPST only communicates with your UPS device via SNMP (default port 161)
|
||||||
|
- No external network connections required after installation
|
||||||
|
|
||||||
## Uninstallation
|
## Uninstallation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If installed from git repository:
|
# Disable and remove service first
|
||||||
cd /path/to/nupst
|
sudo nupst service disable
|
||||||
|
|
||||||
|
# Remove binary and config
|
||||||
|
sudo rm /usr/local/bin/nupst
|
||||||
|
sudo rm /opt/nupst/nupst
|
||||||
|
sudo rm -rf /etc/nupst/
|
||||||
|
|
||||||
|
# Or use the uninstall script if installed from git
|
||||||
sudo ./uninstall.sh
|
sudo ./uninstall.sh
|
||||||
|
|
||||||
# If installed from npm:
|
|
||||||
npm uninstall -g @serve.zone/nupst
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The uninstaller will:
|
|
||||||
- Stop and disable the systemd service (if installed)
|
|
||||||
- Remove the systemd service file
|
|
||||||
- Remove the symlink from /usr/local/bin
|
|
||||||
- Optionally remove configuration files from /etc/nupst
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
### Command Structure (v4.0+)
|
||||||
NUPST - Node.js UPS Shutdown Tool
|
|
||||||
|
NUPST v4.0 uses a subcommand structure for better organization:
|
||||||
|
|
||||||
Usage:
|
|
||||||
nupst enable - Install and enable the systemd service (requires root)
|
|
||||||
nupst disable - Stop and uninstall the systemd service (requires root)
|
|
||||||
nupst daemon-start - Start the daemon process directly
|
|
||||||
nupst logs - Show logs of the systemd service
|
|
||||||
nupst stop - Stop the systemd service
|
|
||||||
nupst start - Start the systemd service
|
|
||||||
nupst status - Show status of the systemd service and UPS status
|
|
||||||
nupst setup - Run the interactive setup to configure SNMP settings
|
|
||||||
nupst help - Show this help message
|
|
||||||
```
|
```
|
||||||
|
NUPST - Network UPS Shutdown Tool
|
||||||
|
Version: 4.0.0
|
||||||
|
|
||||||
|
Usage: nupst <command> [subcommand] [options]
|
||||||
|
|
||||||
|
Service Management:
|
||||||
|
nupst service enable - Install and enable the systemd service
|
||||||
|
nupst service disable - Stop and disable the systemd service
|
||||||
|
nupst service start - Start the systemd service
|
||||||
|
nupst service stop - Stop the systemd service
|
||||||
|
nupst service restart - Restart the systemd service
|
||||||
|
nupst service status - Show service and UPS status
|
||||||
|
nupst service logs - Show service logs in real-time
|
||||||
|
nupst service start-daemon - Start daemon directly (for testing)
|
||||||
|
|
||||||
|
UPS Management:
|
||||||
|
nupst ups add - Add a new UPS device
|
||||||
|
nupst ups edit [id] - Edit a UPS device (prompts if no ID)
|
||||||
|
nupst ups remove <id> - Remove a UPS device by ID
|
||||||
|
nupst ups list - List all configured UPS devices
|
||||||
|
nupst ups test - Test UPS connections
|
||||||
|
|
||||||
|
Group Management:
|
||||||
|
nupst group add - Add a new UPS group
|
||||||
|
nupst group edit <id> - Edit a UPS group
|
||||||
|
nupst group remove <id> - Remove a UPS group
|
||||||
|
nupst group list - List all UPS groups
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
nupst config show - Display current configuration
|
||||||
|
|
||||||
|
Global Options:
|
||||||
|
--version, -v - Show version information
|
||||||
|
--help, -h - Show help message
|
||||||
|
--debug, -d - Enable debug mode for detailed logging
|
||||||
|
|
||||||
|
Aliases (for backward compatibility):
|
||||||
|
nupst ls - Alias for 'nupst ups list'
|
||||||
|
nupst rm <id> - Alias for 'nupst ups remove'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Start Guide
|
||||||
|
|
||||||
|
1. **Install NUPST** (see Installation section above)
|
||||||
|
|
||||||
|
2. **Add your first UPS device:**
|
||||||
|
```bash
|
||||||
|
sudo nupst ups add
|
||||||
|
```
|
||||||
|
Follow the interactive prompts to configure your UPS.
|
||||||
|
|
||||||
|
3. **Test the configuration:**
|
||||||
|
```bash
|
||||||
|
nupst ups test
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Enable the service:**
|
||||||
|
```bash
|
||||||
|
sudo nupst service enable
|
||||||
|
sudo nupst service start
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Check status:**
|
||||||
|
```bash
|
||||||
|
nupst service status
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **View logs:**
|
||||||
|
```bash
|
||||||
|
nupst service logs
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
NUPST provides an interactive setup to configure your UPS:
|
NUPST supports monitoring multiple UPS devices organized into groups. The configuration file is
|
||||||
|
located at `/etc/nupst/config.json`.
|
||||||
|
|
||||||
|
### Interactive Configuration
|
||||||
|
|
||||||
|
The easiest way to configure NUPST is through the interactive commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nupst setup
|
# Add a new UPS device
|
||||||
|
sudo nupst ups add
|
||||||
|
|
||||||
|
# Create a group
|
||||||
|
sudo nupst group add
|
||||||
|
|
||||||
|
# Assign UPS devices to groups
|
||||||
|
sudo nupst group edit <group-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
This will guide you through setting up:
|
### Configuration File Structure
|
||||||
- UPS IP address and SNMP settings
|
|
||||||
- Shutdown thresholds for battery percentage and runtime
|
|
||||||
- Monitoring interval
|
|
||||||
- Test the connection to your UPS
|
|
||||||
|
|
||||||
Alternatively, you can manually edit the configuration file at `/etc/nupst/config.json`. A default configuration will be created on first run:
|
Here's an example configuration with multiple UPS devices in a redundant group:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"snmp": {
|
"checkInterval": 30000,
|
||||||
"host": "127.0.0.1",
|
"upsDevices": [
|
||||||
"port": 161,
|
{
|
||||||
"community": "public",
|
"id": "ups-1",
|
||||||
"version": 1,
|
"name": "Server Room UPS",
|
||||||
"timeout": 5000
|
"snmp": {
|
||||||
},
|
"host": "192.168.1.100",
|
||||||
"thresholds": {
|
"port": 161,
|
||||||
"battery": 60,
|
"community": "public",
|
||||||
"runtime": 20
|
"version": 1,
|
||||||
},
|
"timeout": 5000,
|
||||||
"checkInterval": 30000
|
"upsModel": "cyberpower"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"battery": 60,
|
||||||
|
"runtime": 20
|
||||||
|
},
|
||||||
|
"groups": ["datacenter"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ups-2",
|
||||||
|
"name": "Network Rack UPS",
|
||||||
|
"snmp": {
|
||||||
|
"host": "192.168.1.101",
|
||||||
|
"port": 161,
|
||||||
|
"community": "public",
|
||||||
|
"version": 1,
|
||||||
|
"timeout": 5000,
|
||||||
|
"upsModel": "apc"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"battery": 50,
|
||||||
|
"runtime": 15
|
||||||
|
},
|
||||||
|
"groups": ["datacenter"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": "datacenter",
|
||||||
|
"name": "Data Center",
|
||||||
|
"mode": "redundant",
|
||||||
|
"description": "Main data center UPS group with redundant power"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `snmp`: SNMP connection settings
|
### Configuration Fields
|
||||||
- `host`: IP address of your UPS (default: 127.0.0.1)
|
|
||||||
- `port`: SNMP port (default: 161)
|
#### Global Settings
|
||||||
- `version`: SNMP version (1, 2, or 3)
|
|
||||||
- `timeout`: Timeout in milliseconds (default: 5000)
|
|
||||||
- For SNMPv1/v2c:
|
|
||||||
- `community`: SNMP community string (default: public)
|
|
||||||
- For SNMPv3:
|
|
||||||
- `securityLevel`: Security level ('noAuthNoPriv', 'authNoPriv', or 'authPriv')
|
|
||||||
- `username`: SNMPv3 username
|
|
||||||
- `authProtocol`: Authentication protocol ('MD5' or 'SHA')
|
|
||||||
- `authKey`: Authentication password/key
|
|
||||||
- `privProtocol`: Privacy/encryption protocol ('DES' or 'AES')
|
|
||||||
- `privKey`: Privacy password/key
|
|
||||||
- `thresholds`: When to trigger shutdown
|
|
||||||
- `battery`: Battery percentage threshold (default: 60%)
|
|
||||||
- `runtime`: Runtime minutes threshold (default: 20 minutes)
|
|
||||||
- `checkInterval`: How often to check UPS status in milliseconds (default: 30000)
|
- `checkInterval`: How often to check UPS status in milliseconds (default: 30000)
|
||||||
|
|
||||||
|
#### UPS Device Settings
|
||||||
|
|
||||||
|
- `id`: Unique identifier for the UPS
|
||||||
|
- `name`: Friendly name for the UPS
|
||||||
|
- `groups`: Array of group IDs this UPS belongs to
|
||||||
|
|
||||||
|
**SNMP Configuration:**
|
||||||
|
|
||||||
|
- `host`: IP address or hostname of your UPS
|
||||||
|
- `port`: SNMP port (default: 161)
|
||||||
|
- `version`: SNMP version (1, 2, or 3)
|
||||||
|
- `timeout`: Timeout in milliseconds (default: 5000)
|
||||||
|
- `upsModel`: UPS brand ('cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', or 'custom')
|
||||||
|
|
||||||
|
**For SNMPv1/v2c:**
|
||||||
|
|
||||||
|
- `community`: SNMP community string (default: "public")
|
||||||
|
|
||||||
|
**For SNMPv3:**
|
||||||
|
|
||||||
|
- `securityLevel`: 'noAuthNoPriv', 'authNoPriv', or 'authPriv'
|
||||||
|
- `username`: SNMPv3 username
|
||||||
|
- `authProtocol`: 'MD5' or 'SHA'
|
||||||
|
- `authKey`: Authentication password
|
||||||
|
- `privProtocol`: 'DES' or 'AES' (for authPriv level)
|
||||||
|
- `privKey`: Privacy/encryption password
|
||||||
|
|
||||||
|
**For Custom UPS Models:**
|
||||||
|
|
||||||
|
- `customOIDs`: Custom OID mappings
|
||||||
|
- `POWER_STATUS`: OID for AC power status
|
||||||
|
- `BATTERY_CAPACITY`: OID for battery percentage
|
||||||
|
- `BATTERY_RUNTIME`: OID for runtime remaining (minutes)
|
||||||
|
|
||||||
|
**Shutdown Thresholds:**
|
||||||
|
|
||||||
|
- `battery`: Battery percentage threshold (default: 60%)
|
||||||
|
- `runtime`: Runtime minutes threshold (default: 20 minutes)
|
||||||
|
|
||||||
|
#### Group Settings
|
||||||
|
|
||||||
|
- `id`: Unique identifier for the group
|
||||||
|
- `name`: Friendly name for the group
|
||||||
|
- `mode`: Operating mode ('redundant' or 'nonRedundant')
|
||||||
|
- `description`: Optional description
|
||||||
|
|
||||||
|
### Group Modes
|
||||||
|
|
||||||
|
- **Redundant Mode**: System shuts down only when ALL UPS devices in the group are critical. Ideal
|
||||||
|
for setups with backup UPS units where one can maintain power.
|
||||||
|
|
||||||
|
- **Non-Redundant Mode**: System shuts down when ANY UPS device in the group is critical. Used when
|
||||||
|
all UPS devices must be operational for system stability.
|
||||||
|
|
||||||
## Setup as a Service
|
## Setup as a Service
|
||||||
|
|
||||||
To set up NUPST as a systemd service:
|
Enable NUPST as a systemd service for automatic monitoring:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo nupst enable
|
# Enable and start service
|
||||||
sudo nupst start
|
sudo nupst service enable
|
||||||
|
sudo nupst service start
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
nupst service status
|
||||||
|
|
||||||
|
# View real-time logs
|
||||||
|
nupst service logs
|
||||||
|
|
||||||
|
# Stop service
|
||||||
|
sudo nupst service stop
|
||||||
|
|
||||||
|
# Disable service
|
||||||
|
sudo nupst service disable
|
||||||
```
|
```
|
||||||
|
|
||||||
To check the status:
|
## Updating NUPST
|
||||||
|
|
||||||
|
### Automatic Update
|
||||||
|
|
||||||
|
Re-run the installer to update to the latest version:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nupst status
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
The installer will:
|
||||||
|
|
||||||
MIT
|
1. Download the latest binary
|
||||||
|
2. Replace the existing installation
|
||||||
|
3. Preserve your configuration at `/etc/nupst/config.json`
|
||||||
|
4. Restart the service if it was running
|
||||||
|
|
||||||
|
### Manual Update
|
||||||
|
|
||||||
|
1. Download the latest binary from [releases](https://code.foss.global/serve.zone/nupst/releases)
|
||||||
|
2. Replace the existing binary:
|
||||||
|
```bash
|
||||||
|
sudo nupst service stop
|
||||||
|
sudo mv nupst-linux-x64 /opt/nupst/nupst # adjust for your platform
|
||||||
|
sudo chmod +x /opt/nupst/nupst
|
||||||
|
sudo nupst service start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Checking
|
||||||
|
|
||||||
|
Check your current version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nupst --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
NUPST is designed with security as a priority:
|
||||||
|
|
||||||
|
### Architecture Security
|
||||||
|
|
||||||
|
- **Single Binary**: Self-contained executable with no external dependencies
|
||||||
|
- **No Runtime Dependencies**: Unlike v3.x (Node.js), v4.0+ requires no runtime environment
|
||||||
|
- **Minimal Attack Surface**: Compiled Deno binary with only essential SNMP functionality
|
||||||
|
- **No Supply Chain Risk**: Pre-compiled binaries verified with SHA256 checksums
|
||||||
|
- **Isolated Execution**: Runs with minimal required privileges
|
||||||
|
|
||||||
|
### SNMP Security
|
||||||
|
|
||||||
|
- **SNMPv3 Support**: Full authentication and encryption support
|
||||||
|
- `noAuthNoPriv`: Basic access (no security)
|
||||||
|
- `authNoPriv`: Authentication without encryption
|
||||||
|
- `authPriv`: Full authentication and encryption (recommended)
|
||||||
|
- **Authentication**: MD5 or SHA protocols
|
||||||
|
- **Encryption**: DES or AES privacy protocols
|
||||||
|
- **Secure Defaults**: Automatic timeout adjustment based on security level
|
||||||
|
|
||||||
|
### Installation Security
|
||||||
|
|
||||||
|
- **Checksum Verification**: SHA256SUMS.txt provided for all releases
|
||||||
|
- **Transparent Installation**: Standard locations with clear documentation
|
||||||
|
- **Minimal Permissions**: Only systemd operations require root access
|
||||||
|
- **Source Available**: Full source code available for audit
|
||||||
|
|
||||||
|
### Network Security
|
||||||
|
|
||||||
|
- **Local-Only Communication**: Only connects to UPS devices on local network
|
||||||
|
- **No Telemetry**: No data sent to external servers
|
||||||
|
- **No Update Checks**: Manual update process only
|
||||||
|
|
||||||
|
### Verifying Downloads
|
||||||
|
|
||||||
|
All releases include SHA256 checksums:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download binary and checksums
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/releases/download/v4.0.0/nupst-linux-x64 -o nupst
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/releases/download/v4.0.0/SHA256SUMS.txt -o SHA256SUMS.txt
|
||||||
|
|
||||||
|
# Verify checksum
|
||||||
|
sha256sum -c SHA256SUMS.txt --ignore-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from v3.x
|
||||||
|
|
||||||
|
If you're upgrading from NUPST v3.x (Node.js-based) to v4.0 (Deno-based), the migration is
|
||||||
|
straightforward using the install.sh script.
|
||||||
|
|
||||||
|
### Quick Migration
|
||||||
|
|
||||||
|
The installer script automatically handles the entire migration while preserving your configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the installer (handles stop/update/restart automatically)
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
nupst service status
|
||||||
|
```
|
||||||
|
|
||||||
|
**That's it!** The installer automatically:
|
||||||
|
|
||||||
|
- Detects your v3.x installation
|
||||||
|
- Stops the running service
|
||||||
|
- Replaces the binary with v4.0
|
||||||
|
- Restarts the service
|
||||||
|
- Preserves your `/etc/nupst/config.json` (fully compatible, no changes needed)
|
||||||
|
|
||||||
|
### Key Changes in v4.0
|
||||||
|
|
||||||
|
- **Runtime**: Node.js → Deno
|
||||||
|
- **Distribution**: Git repository + npm packages → Pre-compiled binaries
|
||||||
|
- **Installation**: Clone + setup.sh → Download binary via install.sh
|
||||||
|
- **Dependencies**: Node.js + npm packages → Zero dependencies (self-contained binary)
|
||||||
|
- **CLI Structure**: Flat commands → Subcommand structure (backward compatible)
|
||||||
|
- **Updates**: `nupst update` → Re-run install.sh
|
||||||
|
- **Footprint**: Single ~80MB self-contained binary (vs repo + node_modules in v3.x)
|
||||||
|
- **Startup**: Seconds → Milliseconds
|
||||||
|
|
||||||
|
### Command Mapping
|
||||||
|
|
||||||
|
v4.0 uses a new subcommand structure, but **old commands still work** with deprecation warnings:
|
||||||
|
|
||||||
|
| v3.x Command | v4.0 Command | Notes |
|
||||||
|
| ------------------- | ----------------------- | ---------------------- |
|
||||||
|
| `nupst enable` | `nupst service enable` | Old works with warning |
|
||||||
|
| `nupst disable` | `nupst service disable` | Old works with warning |
|
||||||
|
| `nupst start` | `nupst service start` | Old works with warning |
|
||||||
|
| `nupst stop` | `nupst service stop` | Old works with warning |
|
||||||
|
| `nupst status` | `nupst service status` | Old works with warning |
|
||||||
|
| `nupst logs` | `nupst service logs` | Old works with warning |
|
||||||
|
| `nupst add` | `nupst ups add` | Old works with warning |
|
||||||
|
| `nupst edit [id]` | `nupst ups edit [id]` | Old works with warning |
|
||||||
|
| `nupst delete <id>` | `nupst ups remove <id>` | Old works with warning |
|
||||||
|
| `nupst list` | `nupst ups list` | Old works with warning |
|
||||||
|
| `nupst test` | `nupst ups test` | Old works with warning |
|
||||||
|
| `nupst config` | `nupst config show` | Old works with warning |
|
||||||
|
|
||||||
|
**New aliases:** `nupst ls` (list UPS devices), `nupst rm <id>` (remove UPS device)
|
||||||
|
|
||||||
|
### Configuration Compatibility
|
||||||
|
|
||||||
|
✅ **Fully Compatible:**
|
||||||
|
|
||||||
|
- Configuration file format: `/etc/nupst/config.json`
|
||||||
|
- All SNMP settings (host, port, community, version, security)
|
||||||
|
- UPS device configurations (IDs, names, thresholds, groups)
|
||||||
|
- Group configurations (redundant/non-redundant modes)
|
||||||
|
- Supported UPS models (CyberPower, APC, Eaton, TrippLite, Liebert, custom OIDs)
|
||||||
|
|
||||||
|
### Troubleshooting Migration
|
||||||
|
|
||||||
|
**Service won't start after migration:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Re-enable service to update systemd file
|
||||||
|
sudo nupst service disable
|
||||||
|
sudo nupst service enable
|
||||||
|
sudo nupst service start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Binary won't execute:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chmod +x /opt/nupst/nupst
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command not found:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Recreate symlink
|
||||||
|
sudo ln -sf /opt/nupst/nupst /usr/local/bin/nupst
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Binary Won't Execute
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make sure it's executable
|
||||||
|
chmod +x /opt/nupst/nupst
|
||||||
|
|
||||||
|
# Check architecture matches your system
|
||||||
|
uname -m # Should match binary (x86_64 = x64, aarch64 = arm64)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Won't Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check service status
|
||||||
|
sudo systemctl status nupst
|
||||||
|
|
||||||
|
# Check logs for errors
|
||||||
|
sudo journalctl -u nupst -n 50
|
||||||
|
|
||||||
|
# Verify configuration
|
||||||
|
nupst config show
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't Connect to UPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test SNMP connectivity
|
||||||
|
nupst ups test --debug
|
||||||
|
|
||||||
|
# Check network connectivity
|
||||||
|
ping <ups-ip-address>
|
||||||
|
|
||||||
|
# Verify SNMP port is accessible
|
||||||
|
nc -zv <ups-ip-address> 161
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Denied Errors
|
||||||
|
|
||||||
|
Most operations that modify the system require root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Service management
|
||||||
|
sudo nupst service enable
|
||||||
|
sudo nupst service start
|
||||||
|
|
||||||
|
# Configuration changes
|
||||||
|
sudo nupst ups add
|
||||||
|
sudo nupst group add
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Building from Source
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- [Deno](https://deno.land/) v1.x or later
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://code.foss.global/serve.zone/nupst.git
|
||||||
|
cd nupst
|
||||||
|
|
||||||
|
# Run directly with Deno
|
||||||
|
deno run --allow-all mod.ts help
|
||||||
|
|
||||||
|
# Compile for current platform
|
||||||
|
deno compile --allow-all --output nupst mod.ts
|
||||||
|
|
||||||
|
# Compile for all platforms
|
||||||
|
bash scripts/compile-all.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno test --allow-all tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Issues**: [Report bugs or request features](https://code.foss.global/serve.zone/nupst/issues)
|
||||||
|
- **Documentation**: [Full documentation](https://code.foss.global/serve.zone/nupst)
|
||||||
|
- **Source Code**: [View source](https://code.foss.global/serve.zone/nupst)
|
||||||
|
|
||||||
|
## License and Legal Information
|
||||||
|
|
||||||
|
This repository contains open-source code licensed under the MIT License. A copy of the MIT License
|
||||||
|
can be found in the [license](license) file within this repository.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks,
|
||||||
|
service marks, or product names of the project, except as required for reasonable and customary use
|
||||||
|
in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated
|
||||||
|
with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture
|
||||||
|
Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these
|
||||||
|
trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be
|
||||||
|
approved in writing by Task Venture Capital GmbH.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH Registered at District court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or if you require further information, please contact us via email at
|
||||||
|
hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its
|
||||||
|
terms, and understand that the licensing of the code does not imply endorsement by Task Venture
|
||||||
|
Capital GmbH of any derivative works.
|
||||||
|
613
readme.plan.md
Normal file
613
readme.plan.md
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
# NUPST Migration Plan: Node.js → Deno v4.0.0
|
||||||
|
|
||||||
|
**Migration Goal**: Convert NUPST from Node.js to Deno with single-executable distribution
|
||||||
|
**Version**: 3.1.2 → 4.0.0 (breaking changes) **Platforms**: Linux x64/ARM64, macOS x64/ARM64,
|
||||||
|
Windows x64
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0: Planning & Preparation
|
||||||
|
|
||||||
|
- [x] Research Deno compilation targets and npm: specifier support
|
||||||
|
- [x] Analyze current codebase structure and dependencies
|
||||||
|
- [x] Define CLI command structure simplification
|
||||||
|
- [x] Create detailed migration task list
|
||||||
|
- [ ] Create feature branch: `migration/deno-v4`
|
||||||
|
- [ ] Backup current working state with git tag: `v3.1.2-pre-deno-migration`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Dependency Migration (4-6 hours)
|
||||||
|
|
||||||
|
### 1.1 Analyze Current Dependencies
|
||||||
|
|
||||||
|
- [ ] List all production dependencies from `package.json`
|
||||||
|
- Current: `net-snmp@3.20.0`
|
||||||
|
- [ ] List all dev dependencies to be removed
|
||||||
|
- `@git.zone/tsbuild`, `@git.zone/tsrun`, `@git.zone/tstest`, `@push.rocks/qenv`,
|
||||||
|
`@push.rocks/tapbundle`, `@types/node`
|
||||||
|
- [ ] Identify Node.js built-in module usage
|
||||||
|
- `child_process` (execSync)
|
||||||
|
- `https` (for version checking)
|
||||||
|
- `fs` (readFileSync, writeFileSync, existsSync, mkdirSync)
|
||||||
|
- `path` (join, dirname, resolve)
|
||||||
|
|
||||||
|
### 1.2 Create Deno Configuration
|
||||||
|
|
||||||
|
- [ ] Create `deno.json` with project configuration
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "@serve.zone/nupst",
|
||||||
|
"version": "4.0.0",
|
||||||
|
"exports": "./mod.ts",
|
||||||
|
"tasks": {
|
||||||
|
"dev": "deno run --allow-all mod.ts",
|
||||||
|
"compile": "deno task compile:all",
|
||||||
|
"compile:all": "bash scripts/compile-all.sh",
|
||||||
|
"test": "deno test --allow-all tests/",
|
||||||
|
"check": "deno check mod.ts"
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"rules": {
|
||||||
|
"tags": ["recommended"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fmt": {
|
||||||
|
"useTabs": false,
|
||||||
|
"lineWidth": 100,
|
||||||
|
"indentWidth": 2,
|
||||||
|
"semiColons": true
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["deno.window"],
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"@std/cli": "jsr:@std/cli@^1.0.0",
|
||||||
|
"@std/fmt": "jsr:@std/fmt@^1.0.0",
|
||||||
|
"@std/path": "jsr:@std/path@^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Update Import Statements
|
||||||
|
|
||||||
|
- [ ] `ts/snmp/manager.ts`: Change `import * as snmp from 'net-snmp'` to
|
||||||
|
`import * as snmp from "npm:net-snmp@3.20.0"`
|
||||||
|
- [ ] `ts/cli.ts`: Change `import { execSync } from 'child_process'` to
|
||||||
|
`import { execSync } from "node:child_process"`
|
||||||
|
- [ ] `ts/nupst.ts`: Change `import * as https from 'https'` to
|
||||||
|
`import * as https from "node:https"`
|
||||||
|
- [ ] Search for all `fs` imports and update to `node:fs`
|
||||||
|
- [ ] Search for all `path` imports and update to `node:path`
|
||||||
|
- [ ] Update all relative imports to use `.ts` extension instead of `.js`
|
||||||
|
- Example: `'./nupst.js'` → `'./nupst.ts'`
|
||||||
|
|
||||||
|
### 1.4 Test npm: Specifier Compatibility
|
||||||
|
|
||||||
|
- [ ] Create test file: `tests/snmp_compatibility_test.ts`
|
||||||
|
- [ ] Test SNMP v1 connection with npm:net-snmp
|
||||||
|
- [ ] Test SNMP v2c connection with npm:net-snmp
|
||||||
|
- [ ] Test SNMP v3 connection with npm:net-snmp
|
||||||
|
- [ ] Verify native addon loading works in compiled binary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Code Structure Refactoring (3-4 hours)
|
||||||
|
|
||||||
|
### 2.1 Create Main Entry Point
|
||||||
|
|
||||||
|
- [ ] Create `mod.ts` as main Deno entry point:
|
||||||
|
```typescript
|
||||||
|
#!/usr/bin/env -S deno run --allow-all
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NUPST - UPS Shutdown Tool for Deno
|
||||||
|
*
|
||||||
|
* Required Permissions:
|
||||||
|
* --allow-net: SNMP communication with UPS devices
|
||||||
|
* --allow-read: Configuration file access (/etc/nupst/config.json)
|
||||||
|
* --allow-write: Configuration file updates
|
||||||
|
* --allow-run: System commands (systemctl, shutdown)
|
||||||
|
* --allow-sys: System information (hostname, OS info)
|
||||||
|
* --allow-env: Environment variables
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NupstCli } from './ts/cli.ts';
|
||||||
|
|
||||||
|
const cli = new NupstCli();
|
||||||
|
await cli.parseAndExecute(Deno.args);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Update All Import Extensions
|
||||||
|
|
||||||
|
Files to update (change .js → .ts in imports):
|
||||||
|
|
||||||
|
- [ ] `ts/index.ts`
|
||||||
|
- [ ] `ts/cli.ts` (imports from ./nupst.js, ./logger.js)
|
||||||
|
- [ ] `ts/nupst.ts` (imports from ./snmp/manager.js, ./daemon.js, etc.)
|
||||||
|
- [ ] `ts/daemon.ts` (imports from ./snmp/manager.js, ./logger.js, ./helpers/)
|
||||||
|
- [ ] `ts/systemd.ts` (imports from ./daemon.js, ./logger.js)
|
||||||
|
- [ ] `ts/cli/service-handler.ts`
|
||||||
|
- [ ] `ts/cli/group-handler.ts`
|
||||||
|
- [ ] `ts/cli/ups-handler.ts`
|
||||||
|
- [ ] `ts/snmp/index.ts`
|
||||||
|
- [ ] `ts/snmp/manager.ts` (imports from ./types.js, ./oid-sets.js)
|
||||||
|
- [ ] `ts/snmp/oid-sets.ts` (imports from ./types.js)
|
||||||
|
- [ ] `ts/helpers/index.ts`
|
||||||
|
- [ ] `ts/logger.ts`
|
||||||
|
|
||||||
|
### 2.3 Update process.argv References
|
||||||
|
|
||||||
|
- [ ] `ts/cli.ts`: Replace `process.argv` with `Deno.args` (adjust indexing: process.argv[2] →
|
||||||
|
Deno.args[0])
|
||||||
|
- [ ] Update parseAndExecute method to work with Deno.args (0-indexed vs 2-indexed)
|
||||||
|
|
||||||
|
### 2.4 Update File System Operations
|
||||||
|
|
||||||
|
- [ ] Search for `fs.readFileSync()` → Consider using `Deno.readTextFile()` or keep node:fs
|
||||||
|
- [ ] Search for `fs.writeFileSync()` → Consider using `Deno.writeTextFile()` or keep node:fs
|
||||||
|
- [ ] Search for `fs.existsSync()` → Keep node:fs or use Deno.stat
|
||||||
|
- [ ] Search for `fs.mkdirSync()` → Keep node:fs or use Deno.mkdir
|
||||||
|
- [ ] Decision: Keep node:fs for consistency or migrate to Deno APIs?
|
||||||
|
|
||||||
|
### 2.5 Update Path Operations
|
||||||
|
|
||||||
|
- [ ] Verify all `path.join()`, `path.resolve()`, `path.dirname()` work with node:path
|
||||||
|
- [ ] Consider using `@std/path` from JSR for better Deno integration
|
||||||
|
|
||||||
|
### 2.6 Handle __dirname and __filename
|
||||||
|
|
||||||
|
- [ ] Find all `__dirname` usage
|
||||||
|
- [ ] Replace with `import.meta.dirname` (Deno) or `dirname(fromFileUrl(import.meta.url))`
|
||||||
|
- [ ] Find all `__filename` usage
|
||||||
|
- [ ] Replace with `import.meta.filename` or `fromFileUrl(import.meta.url)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: CLI Command Simplification (3-4 hours)
|
||||||
|
|
||||||
|
### 3.1 Design New Command Structure
|
||||||
|
|
||||||
|
Current → New mapping:
|
||||||
|
|
||||||
|
```
|
||||||
|
OLD NEW
|
||||||
|
=== ===
|
||||||
|
nupst enable → nupst service enable
|
||||||
|
nupst disable → nupst service disable
|
||||||
|
nupst daemon-start → nupst service start-daemon
|
||||||
|
nupst logs → nupst service logs
|
||||||
|
nupst stop → nupst service stop
|
||||||
|
nupst start → nupst service start
|
||||||
|
nupst status → nupst service status
|
||||||
|
|
||||||
|
nupst add → nupst ups add
|
||||||
|
nupst edit [id] → nupst ups edit [id]
|
||||||
|
nupst delete <id> → nupst ups remove <id>
|
||||||
|
nupst list → nupst ups list
|
||||||
|
nupst setup → nupst ups edit (removed alias)
|
||||||
|
nupst test → nupst ups test
|
||||||
|
|
||||||
|
nupst group list → nupst group list
|
||||||
|
nupst group add → nupst group add
|
||||||
|
nupst group edit <id> → nupst group edit <id>
|
||||||
|
nupst group delete <id> → nupst group remove <id>
|
||||||
|
|
||||||
|
nupst config → nupst config show
|
||||||
|
nupst update → nupst update
|
||||||
|
nupst uninstall → nupst uninstall
|
||||||
|
nupst help → nupst help / nupst --help
|
||||||
|
(new) → nupst --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Update CLI Parser (ts/cli.ts)
|
||||||
|
|
||||||
|
- [ ] Refactor `parseAndExecute()` to handle new command structure
|
||||||
|
- [ ] Add `service` subcommand handler
|
||||||
|
- [ ] Add `ups` subcommand handler
|
||||||
|
- [ ] Keep `group` subcommand handler (already exists, just update delete→remove)
|
||||||
|
- [ ] Add `config` subcommand handler with `show` default
|
||||||
|
- [ ] Add `--version` flag handler
|
||||||
|
- [ ] Update `help` command to show new structure
|
||||||
|
- [ ] Add command aliases: `rm` → `remove`, `ls` → `list`
|
||||||
|
- [ ] Add `--json` flag for machine-readable output (future enhancement)
|
||||||
|
|
||||||
|
### 3.3 Update Command Handlers
|
||||||
|
|
||||||
|
- [ ] `ts/cli/service-handler.ts`: Update method names if needed
|
||||||
|
- [ ] `ts/cli/ups-handler.ts`: Rename `delete()` → `remove()`, remove `setup` method
|
||||||
|
- [ ] `ts/cli/group-handler.ts`: Rename `delete()` → `remove()`
|
||||||
|
|
||||||
|
### 3.4 Improve Help Messages
|
||||||
|
|
||||||
|
- [ ] Update `showHelp()` in ts/cli.ts with new command structure
|
||||||
|
- [ ] Update `showGroupHelp()` in ts/cli.ts
|
||||||
|
- [ ] Add `showServiceHelp()` method
|
||||||
|
- [ ] Add `showUpsHelp()` method
|
||||||
|
- [ ] Add `showConfigHelp()` method
|
||||||
|
- [ ] Include usage examples in help text
|
||||||
|
|
||||||
|
### 3.5 Add Version Command
|
||||||
|
|
||||||
|
- [ ] Read version from deno.json
|
||||||
|
- [ ] Create `--version` handler in CLI
|
||||||
|
- [ ] Display version with build info
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Compilation & Distribution (2-3 hours)
|
||||||
|
|
||||||
|
### 4.1 Create Compilation Script
|
||||||
|
|
||||||
|
- [ ] Create directory: `scripts/`
|
||||||
|
- [ ] Create `scripts/compile-all.sh`:
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION=$(cat deno.json | jq -r '.version')
|
||||||
|
BINARY_DIR="dist/binaries"
|
||||||
|
|
||||||
|
echo "Compiling NUPST v${VERSION} for all platforms..."
|
||||||
|
mkdir -p "$BINARY_DIR"
|
||||||
|
|
||||||
|
# Linux x86_64
|
||||||
|
echo "→ Linux x86_64..."
|
||||||
|
deno compile --allow-all --output "$BINARY_DIR/nupst-linux-x64" \
|
||||||
|
--target x86_64-unknown-linux-gnu mod.ts
|
||||||
|
|
||||||
|
# Linux ARM64
|
||||||
|
echo "→ Linux ARM64..."
|
||||||
|
deno compile --allow-all --output "$BINARY_DIR/nupst-linux-arm64" \
|
||||||
|
--target aarch64-unknown-linux-gnu mod.ts
|
||||||
|
|
||||||
|
# macOS x86_64
|
||||||
|
echo "→ macOS x86_64..."
|
||||||
|
deno compile --allow-all --output "$BINARY_DIR/nupst-macos-x64" \
|
||||||
|
--target x86_64-apple-darwin mod.ts
|
||||||
|
|
||||||
|
# macOS ARM64
|
||||||
|
echo "→ macOS ARM64..."
|
||||||
|
deno compile --allow-all --output "$BINARY_DIR/nupst-macos-arm64" \
|
||||||
|
--target aarch64-apple-darwin mod.ts
|
||||||
|
|
||||||
|
# Windows x86_64
|
||||||
|
echo "→ Windows x86_64..."
|
||||||
|
deno compile --allow-all --output "$BINARY_DIR/nupst-windows-x64.exe" \
|
||||||
|
--target x86_64-pc-windows-msvc mod.ts
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Compilation complete!"
|
||||||
|
ls -lh "$BINARY_DIR/"
|
||||||
|
```
|
||||||
|
- [ ] Make script executable: `chmod +x scripts/compile-all.sh`
|
||||||
|
|
||||||
|
### 4.2 Test Local Compilation
|
||||||
|
|
||||||
|
- [ ] Run `deno task compile` to compile for all platforms
|
||||||
|
- [ ] Verify all 5 binaries are created
|
||||||
|
- [ ] Check binary sizes (should be reasonable, < 100MB each)
|
||||||
|
- [ ] Test local binary on current platform: `./dist/binaries/nupst-linux-x64 --version`
|
||||||
|
|
||||||
|
### 4.3 Update Installation Scripts
|
||||||
|
|
||||||
|
- [ ] Update `install.sh`:
|
||||||
|
- Remove Node.js download logic (lines dealing with vendor/node-*)
|
||||||
|
- Add detection for binary download from GitHub releases
|
||||||
|
- Simplify to download appropriate binary based on OS/arch
|
||||||
|
- Place binary in `/opt/nupst/bin/nupst`
|
||||||
|
- Create symlink: `/usr/local/bin/nupst → /opt/nupst/bin/nupst`
|
||||||
|
- Update to v4.0.0 in script
|
||||||
|
- [ ] Simplify or remove `setup.sh` (no longer needed without Node.js)
|
||||||
|
- [ ] Update `bin/nupst` launcher:
|
||||||
|
- Option A: Keep as simple wrapper
|
||||||
|
- Option B: Remove and symlink directly to binary
|
||||||
|
- [ ] Update `uninstall.sh`:
|
||||||
|
- Remove vendor directory cleanup
|
||||||
|
- Update paths to new binary location
|
||||||
|
|
||||||
|
### 4.4 Update Systemd Service
|
||||||
|
|
||||||
|
- [ ] Update systemd service file path in `ts/systemd.ts`
|
||||||
|
- [ ] Verify ExecStart points to correct binary location: `/opt/nupst/bin/nupst daemon-start`
|
||||||
|
- [ ] Remove Node.js environment variables if any
|
||||||
|
- [ ] Test service installation and startup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Testing & Validation (4-6 hours)
|
||||||
|
|
||||||
|
### 5.1 Create Deno Test Suite
|
||||||
|
|
||||||
|
- [ ] Create `tests/` directory (or migrate from existing `test/`)
|
||||||
|
- [ ] Create `tests/snmp_test.ts`: Test SNMP manager functionality
|
||||||
|
- [ ] Create `tests/config_test.ts`: Test configuration loading/saving
|
||||||
|
- [ ] Create `tests/cli_test.ts`: Test CLI parsing and command routing
|
||||||
|
- [ ] Create `tests/daemon_test.ts`: Test daemon logic
|
||||||
|
- [ ] Remove dependency on @git.zone/tstest and @push.rocks/tapbundle
|
||||||
|
- [ ] Use Deno's built-in test runner (`Deno.test()`)
|
||||||
|
|
||||||
|
### 5.2 Unit Tests
|
||||||
|
|
||||||
|
- [ ] Test SNMP connection with mock responses
|
||||||
|
- [ ] Test configuration validation
|
||||||
|
- [ ] Test UPS status parsing for different models
|
||||||
|
- [ ] Test group logic (redundant/non-redundant modes)
|
||||||
|
- [ ] Test threshold checking
|
||||||
|
- [ ] Test version comparison logic
|
||||||
|
|
||||||
|
### 5.3 Integration Tests
|
||||||
|
|
||||||
|
- [ ] Test CLI command parsing for all commands
|
||||||
|
- [ ] Test config file creation and updates
|
||||||
|
- [ ] Test UPS add/edit/remove operations
|
||||||
|
- [ ] Test group add/edit/remove operations
|
||||||
|
- [ ] Mock systemd operations for testing
|
||||||
|
|
||||||
|
### 5.4 Binary Testing
|
||||||
|
|
||||||
|
- [ ] Test compiled binary on Linux x64
|
||||||
|
- [ ] Test compiled binary on Linux ARM64 (if available)
|
||||||
|
- [ ] Test compiled binary on macOS x64 (if available)
|
||||||
|
- [ ] Test compiled binary on macOS ARM64 (if available)
|
||||||
|
- [ ] Test compiled binary on Windows x64 (if available)
|
||||||
|
- [ ] Verify SNMP functionality works in compiled binary
|
||||||
|
- [ ] Verify config file operations work in compiled binary
|
||||||
|
- [ ] Test systemd integration with compiled binary
|
||||||
|
|
||||||
|
### 5.5 Performance Testing
|
||||||
|
|
||||||
|
- [ ] Measure binary size for each platform
|
||||||
|
- [ ] Measure startup time: `time ./nupst-linux-x64 --version`
|
||||||
|
- [ ] Measure memory footprint during daemon operation
|
||||||
|
- [ ] Compare with Node.js version performance
|
||||||
|
- [ ] Document performance metrics
|
||||||
|
|
||||||
|
### 5.6 Upgrade Path Testing
|
||||||
|
|
||||||
|
- [ ] Create test with v3.x config
|
||||||
|
- [ ] Verify v4.x can read existing config
|
||||||
|
- [ ] Test migration from old commands to new commands
|
||||||
|
- [ ] Verify systemd service upgrade path
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Distribution Strategy (2-3 hours)
|
||||||
|
|
||||||
|
### 6.1 GitHub Actions Workflow
|
||||||
|
|
||||||
|
- [ ] Create `.github/workflows/release.yml`:
|
||||||
|
```yaml
|
||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: denoland/setup-deno@v1
|
||||||
|
with:
|
||||||
|
deno-version: v1.x
|
||||||
|
- name: Compile binaries
|
||||||
|
run: deno task compile
|
||||||
|
- name: Generate checksums
|
||||||
|
run: |
|
||||||
|
cd dist/binaries
|
||||||
|
sha256sum * > SHA256SUMS
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: dist/binaries/*
|
||||||
|
generate_release_notes: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Update package.json for npm
|
||||||
|
|
||||||
|
- [ ] Update version to 4.0.0
|
||||||
|
- [ ] Update description to mention Deno
|
||||||
|
- [ ] Add postinstall script to symlink appropriate binary:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "@serve.zone/nupst",
|
||||||
|
"version": "4.0.0",
|
||||||
|
"description": "UPS Shutdown Tool - Deno-based single executable",
|
||||||
|
"bin": {
|
||||||
|
"nupst": "bin/nupst-npm-wrapper.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "node bin/setup-npm-binary.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/binaries/*",
|
||||||
|
"bin/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [ ] Create `bin/setup-npm-binary.js` to symlink correct binary
|
||||||
|
- [ ] Create `bin/nupst-npm-wrapper.js` as entry point
|
||||||
|
|
||||||
|
### 6.3 Verify Distribution Methods
|
||||||
|
|
||||||
|
- [ ] Test GitHub release download and installation
|
||||||
|
- [ ] Test npm install from tarball
|
||||||
|
- [ ] Test direct install.sh script
|
||||||
|
- [ ] Verify all methods create working installation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: Documentation Updates (2-3 hours)
|
||||||
|
|
||||||
|
### 7.1 Update README.md
|
||||||
|
|
||||||
|
- [ ] Remove Node.js requirements section
|
||||||
|
- [ ] Update features list (mention Deno, single executable)
|
||||||
|
- [ ] Update installation methods:
|
||||||
|
- Method 1: Quick install script (updated)
|
||||||
|
- Method 2: GitHub releases (new)
|
||||||
|
- Method 3: npm (updated with notes)
|
||||||
|
- [ ] Update usage section with new command structure
|
||||||
|
- [ ] Add command mapping table (v3 → v4)
|
||||||
|
- [ ] Update platform support matrix (note: no Windows ARM)
|
||||||
|
- [ ] Update "System Changes" section (no vendor directory)
|
||||||
|
- [ ] Update security section (remove Node.js mentions)
|
||||||
|
- [ ] Update uninstallation instructions
|
||||||
|
|
||||||
|
### 7.2 Create MIGRATION.md
|
||||||
|
|
||||||
|
- [ ] Create detailed migration guide from v3.x to v4.x
|
||||||
|
- [ ] List all breaking changes:
|
||||||
|
1. CLI command structure reorganization
|
||||||
|
2. No Node.js requirement
|
||||||
|
3. Windows ARM not supported
|
||||||
|
4. Installation path changes
|
||||||
|
- [ ] Provide command mapping table
|
||||||
|
- [ ] Explain config compatibility
|
||||||
|
- [ ] Document upgrade procedure
|
||||||
|
- [ ] Add rollback instructions
|
||||||
|
|
||||||
|
### 7.3 Update CHANGELOG.md
|
||||||
|
|
||||||
|
- [ ] Add v4.0.0 section with all breaking changes
|
||||||
|
- [ ] List new features (Deno, single executable)
|
||||||
|
- [ ] List improvements (startup time, binary size)
|
||||||
|
- [ ] List removed features (Windows ARM, setup command alias)
|
||||||
|
- [ ] Migration guide reference
|
||||||
|
|
||||||
|
### 7.4 Update Help Text
|
||||||
|
|
||||||
|
- [ ] Ensure all help commands show new structure
|
||||||
|
- [ ] Add examples for common operations
|
||||||
|
- [ ] Include migration notes in help output
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 8: Cleanup & Finalization (1 hour)
|
||||||
|
|
||||||
|
### 8.1 Remove Obsolete Files
|
||||||
|
|
||||||
|
- [ ] Delete `vendor/` directory (Node.js binaries)
|
||||||
|
- [ ] Delete `dist/` directory (old compiled JS)
|
||||||
|
- [ ] Delete `dist_ts/` directory (old compiled TS)
|
||||||
|
- [ ] Delete `node_modules/` directory
|
||||||
|
- [ ] Remove or update `tsconfig.json` (decide if needed for npm compatibility)
|
||||||
|
- [ ] Remove `setup.sh` if no longer needed
|
||||||
|
- [ ] Remove old test files in `test/` if migrated to `tests/`
|
||||||
|
- [ ] Delete `pnpm-lock.yaml`
|
||||||
|
|
||||||
|
### 8.2 Update Git Configuration
|
||||||
|
|
||||||
|
- [ ] Update `.gitignore`:
|
||||||
|
```
|
||||||
|
# Deno
|
||||||
|
.deno/
|
||||||
|
deno.lock
|
||||||
|
|
||||||
|
# Compiled binaries
|
||||||
|
dist/binaries/
|
||||||
|
|
||||||
|
# Old Node.js artifacts (to be removed)
|
||||||
|
node_modules/
|
||||||
|
vendor/
|
||||||
|
dist/
|
||||||
|
dist_ts/
|
||||||
|
pnpm-lock.yaml
|
||||||
|
```
|
||||||
|
- [ ] Add `deno.lock` to version control
|
||||||
|
- [ ] Create `.denoignore` if needed
|
||||||
|
|
||||||
|
### 8.3 Final Validation
|
||||||
|
|
||||||
|
- [ ] Run `deno check mod.ts` - verify no type errors
|
||||||
|
- [ ] Run `deno lint` - verify code quality
|
||||||
|
- [ ] Run `deno fmt --check` - verify formatting
|
||||||
|
- [ ] Run `deno task test` - verify all tests pass
|
||||||
|
- [ ] Run `deno task compile` - verify all binaries compile
|
||||||
|
- [ ] Test each binary manually
|
||||||
|
|
||||||
|
### 8.4 Prepare for Release
|
||||||
|
|
||||||
|
- [ ] Create git tag: `v4.0.0`
|
||||||
|
- [ ] Push to main branch
|
||||||
|
- [ ] Push tags to trigger release workflow
|
||||||
|
- [ ] Verify GitHub Actions workflow succeeds
|
||||||
|
- [ ] Verify binaries are attached to release
|
||||||
|
- [ ] Test installation from GitHub release
|
||||||
|
- [ ] Publish to npm: `npm publish`
|
||||||
|
- [ ] Test npm installation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Strategy
|
||||||
|
|
||||||
|
If critical issues are discovered:
|
||||||
|
|
||||||
|
- [ ] Keep `v3.1.2` tag available for rollback
|
||||||
|
- [ ] Create `v3-stable` branch for continued v3 maintenance
|
||||||
|
- [ ] Update install.sh to offer v3/v4 choice
|
||||||
|
- [ ] Document known issues in GitHub Issues
|
||||||
|
- [ ] Provide downgrade instructions in docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria Checklist
|
||||||
|
|
||||||
|
- [ ] ✅ All 5 platform binaries compile successfully
|
||||||
|
- [ ] ✅ Binary sizes are reasonable (< 100MB per platform)
|
||||||
|
- [ ] ✅ Startup time < 2 seconds
|
||||||
|
- [ ] ✅ SNMP v1/v2c/v3 functionality verified on real UPS device
|
||||||
|
- [ ] ✅ All CLI commands work with new structure
|
||||||
|
- [ ] ✅ Config file compatibility maintained
|
||||||
|
- [ ] ✅ Systemd integration works on Linux
|
||||||
|
- [ ] ✅ Installation scripts work on fresh systems
|
||||||
|
- [ ] ✅ npm package still installable and functional
|
||||||
|
- [ ] ✅ All tests pass
|
||||||
|
- [ ] ✅ Documentation is complete and accurate
|
||||||
|
- [ ] ✅ GitHub release created with binaries
|
||||||
|
- [ ] ✅ Migration guide tested by following it step-by-step
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
- **Phase 0**: 1 hour ✓ (in progress)
|
||||||
|
- **Phase 1**: 4-6 hours
|
||||||
|
- **Phase 2**: 3-4 hours
|
||||||
|
- **Phase 3**: 3-4 hours
|
||||||
|
- **Phase 4**: 2-3 hours
|
||||||
|
- **Phase 5**: 4-6 hours
|
||||||
|
- **Phase 6**: 2-3 hours
|
||||||
|
- **Phase 7**: 2-3 hours
|
||||||
|
- **Phase 8**: 1 hour
|
||||||
|
|
||||||
|
**Total Estimate**: 22-31 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes & Decisions
|
||||||
|
|
||||||
|
### Key Decisions Made:
|
||||||
|
|
||||||
|
1. ✅ Use npm:net-snmp (no pure Deno SNMP library available)
|
||||||
|
2. ✅ Major version bump to 4.0.0 (breaking changes)
|
||||||
|
3. ✅ CLI reorganization with subcommands
|
||||||
|
4. ✅ Keep npm publishing alongside binary distribution
|
||||||
|
5. ✅ 5 platform targets (Windows ARM not supported by Deno yet)
|
||||||
|
|
||||||
|
### Open Questions:
|
||||||
|
|
||||||
|
- [ ] Should we keep tsconfig.json for npm package compatibility?
|
||||||
|
- [ ] Should we fully migrate to Deno APIs (Deno.readFile) or keep node:fs?
|
||||||
|
- [ ] Should we remove the `bin/nupst` wrapper or keep it?
|
||||||
|
- [ ] Should setup.sh be completely removed or kept for dependencies?
|
||||||
|
|
||||||
|
### Risk Areas:
|
||||||
|
|
||||||
|
- ⚠️ SNMP native addon compatibility in compiled binaries (HIGH PRIORITY TO TEST)
|
||||||
|
- ⚠️ Systemd integration with new binary structure
|
||||||
|
- ⚠️ Config migration from v3 to v4
|
||||||
|
- ⚠️ npm package installation with embedded binaries
|
66
scripts/compile-all.sh
Executable file
66
scripts/compile-all.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Get version from deno.json
|
||||||
|
VERSION=$(cat deno.json | grep -o '"version": *"[^"]*"' | cut -d'"' -f4)
|
||||||
|
BINARY_DIR="dist/binaries"
|
||||||
|
|
||||||
|
echo "================================================"
|
||||||
|
echo " NUPST Compilation Script"
|
||||||
|
echo " Version: ${VERSION}"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
echo "Compiling for all supported platforms..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clean up old binaries and create fresh directory
|
||||||
|
rm -rf "$BINARY_DIR"
|
||||||
|
mkdir -p "$BINARY_DIR"
|
||||||
|
echo "→ Cleaned old binaries from $BINARY_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Linux x86_64
|
||||||
|
echo "→ Compiling for Linux x86_64..."
|
||||||
|
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-linux-x64" \
|
||||||
|
--target x86_64-unknown-linux-gnu mod.ts
|
||||||
|
echo " ✓ Linux x86_64 complete"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Linux ARM64
|
||||||
|
echo "→ Compiling for Linux ARM64..."
|
||||||
|
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-linux-arm64" \
|
||||||
|
--target aarch64-unknown-linux-gnu mod.ts
|
||||||
|
echo " ✓ Linux ARM64 complete"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# macOS x86_64
|
||||||
|
echo "→ Compiling for macOS x86_64..."
|
||||||
|
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-macos-x64" \
|
||||||
|
--target x86_64-apple-darwin mod.ts
|
||||||
|
echo " ✓ macOS x86_64 complete"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# macOS ARM64
|
||||||
|
echo "→ Compiling for macOS ARM64..."
|
||||||
|
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-macos-arm64" \
|
||||||
|
--target aarch64-apple-darwin mod.ts
|
||||||
|
echo " ✓ macOS ARM64 complete"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Windows x86_64
|
||||||
|
echo "→ Compiling for Windows x86_64..."
|
||||||
|
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-windows-x64.exe" \
|
||||||
|
--target x86_64-pc-windows-msvc mod.ts
|
||||||
|
echo " ✓ Windows x86_64 complete"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "================================================"
|
||||||
|
echo " Compilation Summary"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
ls -lh "$BINARY_DIR/" | tail -n +2
|
||||||
|
echo ""
|
||||||
|
echo "✓ All binaries compiled successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Binary location: $BINARY_DIR/"
|
||||||
|
echo ""
|
227
setup.sh
227
setup.sh
@@ -1,227 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# NUPST Setup Script
|
|
||||||
# Downloads the appropriate Node.js binary for the current platform
|
|
||||||
|
|
||||||
# Find the directory where this script is located
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
|
|
||||||
# Create vendor directory if it doesn't exist
|
|
||||||
mkdir -p "$SCRIPT_DIR/vendor"
|
|
||||||
|
|
||||||
# Get the latest LTS Node.js version
|
|
||||||
echo "Determining latest LTS Node.js version..."
|
|
||||||
NODE_VERSIONS_JSON=$(curl -s https://nodejs.org/dist/index.json)
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Warning: Could not fetch latest Node.js versions. Using fallback version."
|
|
||||||
NODE_VERSION="20.11.1" # Fallback to a recent LTS version
|
|
||||||
else
|
|
||||||
# Extract the latest LTS version (those marked with lts field)
|
|
||||||
NODE_VERSION=$(echo "$NODE_VERSIONS_JSON" | grep -o '"version":"v[0-9.]*".*"lts":[^,]*' | grep -v '"lts":false' | grep -o 'v[0-9.]*' | head -1 | cut -c 2-)
|
|
||||||
|
|
||||||
if [ -z "$NODE_VERSION" ]; then
|
|
||||||
echo "Warning: Could not determine latest LTS version. Using fallback version."
|
|
||||||
NODE_VERSION="20.11.1" # Fallback to a recent LTS version
|
|
||||||
else
|
|
||||||
echo "Latest Node.js LTS version: $NODE_VERSION"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Detect architecture
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
OS=$(uname -s)
|
|
||||||
|
|
||||||
# Map architecture and OS to Node.js download URL
|
|
||||||
NODE_URL=""
|
|
||||||
NODE_DIR=""
|
|
||||||
case "$OS" in
|
|
||||||
Linux)
|
|
||||||
case "$ARCH" in
|
|
||||||
x86_64)
|
|
||||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz"
|
|
||||||
NODE_DIR="node-linux-x64"
|
|
||||||
;;
|
|
||||||
aarch64|arm64)
|
|
||||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-arm64.tar.gz"
|
|
||||||
NODE_DIR="node-linux-arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: $ARCH. Please install Node.js manually."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
Darwin)
|
|
||||||
case "$ARCH" in
|
|
||||||
x86_64)
|
|
||||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-x64.tar.gz"
|
|
||||||
NODE_DIR="node-darwin-x64"
|
|
||||||
;;
|
|
||||||
arm64)
|
|
||||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-arm64.tar.gz"
|
|
||||||
NODE_DIR="node-darwin-arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: $ARCH. Please install Node.js manually."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported operating system: $OS. Please install Node.js manually."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Check if we already have the Node.js binary
|
|
||||||
if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ]; then
|
|
||||||
echo "Node.js binary already exists for $OS-$ARCH. Skipping download."
|
|
||||||
else
|
|
||||||
echo "Downloading Node.js v$NODE_VERSION for $OS-$ARCH..."
|
|
||||||
|
|
||||||
# Download and extract Node.js
|
|
||||||
TMP_FILE="$SCRIPT_DIR/vendor/node.tar.gz"
|
|
||||||
curl -L "$NODE_URL" -o "$TMP_FILE"
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Error downloading Node.js. Please check your internet connection and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create target directory
|
|
||||||
mkdir -p "$SCRIPT_DIR/vendor/$NODE_DIR"
|
|
||||||
|
|
||||||
# Extract Node.js
|
|
||||||
tar -xzf "$TMP_FILE" -C "$SCRIPT_DIR/vendor"
|
|
||||||
|
|
||||||
# Move extracted files to the target directory
|
|
||||||
NODE_EXTRACT_DIR=$(find "$SCRIPT_DIR/vendor" -maxdepth 1 -name "node-v*" -type d | head -n 1)
|
|
||||||
if [ -d "$NODE_EXTRACT_DIR" ]; then
|
|
||||||
cp -R "$NODE_EXTRACT_DIR"/* "$SCRIPT_DIR/vendor/$NODE_DIR/"
|
|
||||||
rm -rf "$NODE_EXTRACT_DIR"
|
|
||||||
else
|
|
||||||
echo "Error extracting Node.js. Please try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
rm "$TMP_FILE"
|
|
||||||
|
|
||||||
echo "Node.js v$NODE_VERSION for $OS-$ARCH has been downloaded and extracted."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove any existing dist_ts directory
|
|
||||||
if [ -d "$SCRIPT_DIR/dist_ts" ]; then
|
|
||||||
echo "Removing existing dist_ts directory..."
|
|
||||||
rm -rf "$SCRIPT_DIR/dist_ts"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Download dist_ts from npm registry
|
|
||||||
echo "Downloading dist_ts from npm registry..."
|
|
||||||
|
|
||||||
# Create temp directory
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
|
|
||||||
# Get version from package.json
|
|
||||||
if [ -f "$SCRIPT_DIR/package.json" ]; then
|
|
||||||
echo "Reading version from package.json..."
|
|
||||||
# Extract version using grep and cut
|
|
||||||
VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json" | cut -d'"' -f4)
|
|
||||||
|
|
||||||
if [ -z "$VERSION" ]; then
|
|
||||||
echo "Error: Could not determine version from package.json."
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Package version is $VERSION. Downloading matching package tarball..."
|
|
||||||
else
|
|
||||||
echo "Warning: package.json not found. Getting latest version from npm registry..."
|
|
||||||
VERSION=$(curl -s https://registry.npmjs.org/@serve.zone/nupst | grep -o '"latest":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
|
|
||||||
if [ -z "$VERSION" ]; then
|
|
||||||
echo "Error: Could not determine version from npm registry."
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Latest version is $VERSION. Using as fallback."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# First try to download with the version from package.json
|
|
||||||
TARBALL_URL="https://registry.npmjs.org/@serve.zone/nupst/-/nupst-$VERSION.tgz"
|
|
||||||
TARBALL_PATH="$TEMP_DIR/nupst.tgz"
|
|
||||||
|
|
||||||
echo "Attempting to download version $VERSION from $TARBALL_URL..."
|
|
||||||
curl -sL "$TARBALL_URL" -o "$TARBALL_PATH"
|
|
||||||
|
|
||||||
# If download fails or file is empty, try to get the latest version from npm
|
|
||||||
if [ $? -ne 0 ] || [ ! -s "$TARBALL_PATH" ]; then
|
|
||||||
echo "Package version $VERSION not found on npm registry."
|
|
||||||
echo "Fetching latest version information from npm registry..."
|
|
||||||
|
|
||||||
# Get latest version from npm registry
|
|
||||||
NPM_REGISTRY_INFO=$(curl -s https://registry.npmjs.org/@serve.zone/nupst)
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Error: Could not connect to npm registry."
|
|
||||||
echo "Will attempt to build from source instead."
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
mkdir -p "$SCRIPT_DIR/dist_ts"
|
|
||||||
BUILD_FROM_SOURCE=1
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract latest version
|
|
||||||
LATEST_VERSION=$(echo "$NPM_REGISTRY_INFO" | grep -o '"latest":"[^"]*"' | cut -d'"' -f4)
|
|
||||||
|
|
||||||
if [ -z "$LATEST_VERSION" ]; then
|
|
||||||
echo "Error: Could not determine latest version from npm registry."
|
|
||||||
echo "Will attempt to build from source instead."
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
mkdir -p "$SCRIPT_DIR/dist_ts"
|
|
||||||
BUILD_FROM_SOURCE=1
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Found latest version: $LATEST_VERSION. Downloading..."
|
|
||||||
|
|
||||||
TARBALL_URL="https://registry.npmjs.org/@serve.zone/nupst/-/nupst-$LATEST_VERSION.tgz"
|
|
||||||
TARBALL_PATH="$TEMP_DIR/nupst.tgz"
|
|
||||||
|
|
||||||
curl -sL "$TARBALL_URL" -o "$TARBALL_PATH"
|
|
||||||
|
|
||||||
if [ $? -ne 0 ] || [ ! -s "$TARBALL_PATH" ]; then
|
|
||||||
echo "Error: Failed to download any package version from npm registry."
|
|
||||||
echo "Installation cannot continue without the dist_ts directory."
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract the tarball
|
|
||||||
mkdir -p "$TEMP_DIR/extract"
|
|
||||||
tar -xzf "$TARBALL_PATH" -C "$TEMP_DIR/extract"
|
|
||||||
|
|
||||||
# Copy dist_ts to the installation directory
|
|
||||||
if [ -d "$TEMP_DIR/extract/package/dist_ts" ]; then
|
|
||||||
echo "Copying dist_ts directory to installation..."
|
|
||||||
mkdir -p "$SCRIPT_DIR/dist_ts"
|
|
||||||
cp -R "$TEMP_DIR/extract/package/dist_ts/"* "$SCRIPT_DIR/dist_ts/"
|
|
||||||
else
|
|
||||||
echo "Error: dist_ts directory not found in the downloaded npm package."
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
|
|
||||||
echo "dist_ts directory successfully downloaded from npm registry."
|
|
||||||
|
|
||||||
# Make launcher script executable
|
|
||||||
chmod +x "$SCRIPT_DIR/bin/nupst"
|
|
||||||
|
|
||||||
echo "NUPST setup completed successfully."
|
|
||||||
echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst"
|
|
||||||
echo "To install NUPST globally, run: sudo ln -s $SCRIPT_DIR/bin/nupst /usr/local/bin/nupst"
|
|
168
test/manualdocker/00-test-fresh-v4-install.sh
Executable file
168
test/manualdocker/00-test-fresh-v4-install.sh
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Test fresh v4 installation from scratch
|
||||||
|
# Tests the most common user scenario: clean install using curl | bash
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONTAINER_NAME="nupst-test-fresh-v4"
|
||||||
|
|
||||||
|
echo "================================================"
|
||||||
|
echo " NUPST Fresh v4 Installation Test"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if container already exists
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
echo "⚠️ Container ${CONTAINER_NAME} already exists"
|
||||||
|
read -p "Remove and recreate? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "→ Stopping and removing existing container..."
|
||||||
|
docker stop ${CONTAINER_NAME} 2>/dev/null || true
|
||||||
|
docker rm ${CONTAINER_NAME} 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "Exiting. Remove manually with: docker rm -f ${CONTAINER_NAME}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ Creating Docker container with systemd..."
|
||||||
|
docker run -d \
|
||||||
|
--name ${CONTAINER_NAME} \
|
||||||
|
--privileged \
|
||||||
|
--cgroupns=host \
|
||||||
|
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
|
||||||
|
ubuntu:22.04 \
|
||||||
|
/bin/bash -c "apt-get update && apt-get install -y systemd systemd-sysv && exec /sbin/init"
|
||||||
|
|
||||||
|
echo "→ Waiting for systemd to initialize..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
echo "→ Waiting for dpkg lock to be released..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
|
||||||
|
echo ' Waiting for dpkg lock...'
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo ' dpkg lock released'
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "→ Installing prerequisites (curl)..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq curl
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "→ Installing NUPST v4 using curl | bash..."
|
||||||
|
echo " Command: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "================================================"
|
||||||
|
echo " Verifying Installation"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "→ Checking binary location..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
if [ -f /opt/nupst/nupst ]; then
|
||||||
|
echo ' ✓ Binary exists at /opt/nupst/nupst'
|
||||||
|
ls -lh /opt/nupst/nupst
|
||||||
|
else
|
||||||
|
echo ' ✗ Binary not found at /opt/nupst/nupst'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "→ Checking symlink..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
if [ -L /usr/local/bin/nupst ]; then
|
||||||
|
echo ' ✓ Symlink exists at /usr/local/bin/nupst'
|
||||||
|
ls -lh /usr/local/bin/nupst
|
||||||
|
elif [ -L /usr/bin/nupst ]; then
|
||||||
|
echo ' ✓ Symlink exists at /usr/bin/nupst'
|
||||||
|
ls -lh /usr/bin/nupst
|
||||||
|
else
|
||||||
|
echo ' ✗ Symlink not found in /usr/local/bin or /usr/bin'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "→ Checking PATH integration..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
NUPST_PATH=\$(which nupst 2>/dev/null)
|
||||||
|
if [ -n \"\$NUPST_PATH\" ]; then
|
||||||
|
echo ' ✓ nupst found in PATH at: '\$NUPST_PATH
|
||||||
|
else
|
||||||
|
echo ' ✗ nupst not found in PATH'
|
||||||
|
echo ' PATH contents:'
|
||||||
|
echo \$PATH
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "→ Testing nupst command execution..."
|
||||||
|
docker exec ${CONTAINER_NAME} nupst --version
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "→ Creating minimal config for service test..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
mkdir -p /etc/nupst
|
||||||
|
cat > /etc/nupst/config.json << 'EOF'
|
||||||
|
{
|
||||||
|
\"version\": \"4.0\",
|
||||||
|
\"upsDevices\": [],
|
||||||
|
\"groups\": [],
|
||||||
|
\"checkInterval\": 30000
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
echo ' ✓ Minimal config created'
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "→ Testing service creation..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
echo ' Running: nupst service enable'
|
||||||
|
nupst service enable
|
||||||
|
|
||||||
|
if [ -f /etc/systemd/system/nupst.service ]; then
|
||||||
|
echo ' ✓ Service file created successfully'
|
||||||
|
else
|
||||||
|
echo ' ✗ Service file creation failed'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "→ Checking if service is enabled..."
|
||||||
|
docker exec ${CONTAINER_NAME} systemctl is-enabled nupst
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "================================================"
|
||||||
|
echo " ✓ Fresh v4 Installation Test Complete"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
echo "Installation verified successfully:"
|
||||||
|
echo " • Binary installed to /opt/nupst/nupst"
|
||||||
|
echo " • Symlink created for global access"
|
||||||
|
echo " • nupst command available in PATH"
|
||||||
|
echo " • Command executes correctly"
|
||||||
|
echo " • Systemd service file created"
|
||||||
|
echo ""
|
||||||
|
echo "Useful commands:"
|
||||||
|
echo " docker exec -it ${CONTAINER_NAME} bash"
|
||||||
|
echo " docker exec ${CONTAINER_NAME} nupst --help"
|
||||||
|
echo " docker exec ${CONTAINER_NAME} nupst service status"
|
||||||
|
echo " docker stop ${CONTAINER_NAME}"
|
||||||
|
echo " docker rm -f ${CONTAINER_NAME}"
|
||||||
|
echo ""
|
148
test/manualdocker/01-setup-v3-container.sh
Executable file
148
test/manualdocker/01-setup-v3-container.sh
Executable file
@@ -0,0 +1,148 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Setup Docker container with systemd and install NUPST v3
|
||||||
|
# This creates a container from commit 806f81c6a057a2a5da586b96a231d391f12eb1bb (v3)
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONTAINER_NAME="nupst-test-v3"
|
||||||
|
V3_COMMIT="806f81c6a057a2a5da586b96a231d391f12eb1bb"
|
||||||
|
|
||||||
|
echo "================================================"
|
||||||
|
echo " NUPST v3 Test Container Setup"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if container already exists
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
echo "⚠️ Container ${CONTAINER_NAME} already exists"
|
||||||
|
read -p "Remove and recreate? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "→ Stopping and removing existing container..."
|
||||||
|
docker stop ${CONTAINER_NAME} 2>/dev/null || true
|
||||||
|
docker rm ${CONTAINER_NAME} 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "Exiting. Remove manually with: docker rm -f ${CONTAINER_NAME}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ Creating Docker container (will install systemd)..."
|
||||||
|
docker run -d \
|
||||||
|
--name ${CONTAINER_NAME} \
|
||||||
|
--privileged \
|
||||||
|
--cgroupns=host \
|
||||||
|
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
|
||||||
|
ubuntu:22.04 \
|
||||||
|
/bin/bash -c "apt-get update && apt-get install -y systemd systemd-sysv && exec /sbin/init"
|
||||||
|
|
||||||
|
echo "→ Waiting for systemd to initialize..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
echo "→ Waiting for dpkg lock to be released..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
|
||||||
|
echo ' Waiting for dpkg lock...'
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo ' dpkg lock released'
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "→ Installing prerequisites in container..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq git curl sudo jq
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "→ Cloning NUPST v3 (commit ${V3_COMMIT})..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
cd /opt
|
||||||
|
git clone https://code.foss.global/serve.zone/nupst.git
|
||||||
|
cd nupst
|
||||||
|
git checkout ${V3_COMMIT}
|
||||||
|
echo 'Checked out commit:'
|
||||||
|
git log -1 --oneline
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "→ Running NUPST v3 installation directly (bypassing install.sh auto-update)..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
cd /opt/nupst
|
||||||
|
# Run setup.sh directly to avoid install.sh trying to update to v4
|
||||||
|
bash setup.sh -y
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "→ Creating NUPST configuration using real UPS data from .nogit/env.json..."
|
||||||
|
|
||||||
|
# Check if .nogit/env.json exists
|
||||||
|
if [ ! -f "../../.nogit/env.json" ]; then
|
||||||
|
echo "❌ Error: .nogit/env.json not found"
|
||||||
|
echo "This file contains test UPS credentials and is required for testing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read UPS data from .nogit/env.json and create v3 config
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "mkdir -p /etc/nupst"
|
||||||
|
|
||||||
|
# Generate config from .nogit/env.json using jq
|
||||||
|
cat ../../.nogit/env.json | jq -r '
|
||||||
|
{
|
||||||
|
"upsList": [
|
||||||
|
{
|
||||||
|
"id": "test-ups-v1",
|
||||||
|
"name": "Test UPS (SNMP v1)",
|
||||||
|
"host": .testConfigV1.snmp.host,
|
||||||
|
"port": .testConfigV1.snmp.port,
|
||||||
|
"community": .testConfigV1.snmp.community,
|
||||||
|
"version": (.testConfigV1.snmp.version | tostring),
|
||||||
|
"batteryLowOID": "1.3.6.1.4.1.935.1.1.1.3.3.1.0",
|
||||||
|
"onBatteryOID": "1.3.6.1.4.1.935.1.1.1.3.3.2.0",
|
||||||
|
"shutdownCommand": "echo \"Shutdown triggered for test-ups-v1\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test-ups-v3",
|
||||||
|
"name": "Test UPS (SNMP v3)",
|
||||||
|
"host": .testConfigV3.snmp.host,
|
||||||
|
"port": .testConfigV3.snmp.port,
|
||||||
|
"version": (.testConfigV3.snmp.version | tostring),
|
||||||
|
"securityLevel": .testConfigV3.snmp.securityLevel,
|
||||||
|
"username": .testConfigV3.snmp.username,
|
||||||
|
"authProtocol": .testConfigV3.snmp.authProtocol,
|
||||||
|
"authKey": .testConfigV3.snmp.authKey,
|
||||||
|
"batteryLowOID": "1.3.6.1.4.1.935.1.1.1.3.3.1.0",
|
||||||
|
"onBatteryOID": "1.3.6.1.4.1.935.1.1.1.3.3.2.0",
|
||||||
|
"shutdownCommand": "echo \"Shutdown triggered for test-ups-v3\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groups": []
|
||||||
|
}' | docker exec -i ${CONTAINER_NAME} tee /etc/nupst/config.json > /dev/null
|
||||||
|
|
||||||
|
echo " ✓ Real UPS config created at /etc/nupst/config.json (from .nogit/env.json)"
|
||||||
|
|
||||||
|
echo "→ Enabling NUPST systemd service..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
nupst enable
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "→ Starting NUPST service..."
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
nupst start
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "================================================"
|
||||||
|
echo " ✓ NUPST v3 Container Ready"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
echo "Container name: ${CONTAINER_NAME}"
|
||||||
|
echo "NUPST version: v3 (commit ${V3_COMMIT})"
|
||||||
|
echo ""
|
||||||
|
echo "Useful commands:"
|
||||||
|
echo " docker exec -it ${CONTAINER_NAME} bash"
|
||||||
|
echo " docker exec ${CONTAINER_NAME} systemctl status nupst"
|
||||||
|
echo " docker exec ${CONTAINER_NAME} nupst --version"
|
||||||
|
echo " docker stop ${CONTAINER_NAME}"
|
||||||
|
echo " docker start ${CONTAINER_NAME}"
|
||||||
|
echo " docker rm -f ${CONTAINER_NAME}"
|
||||||
|
echo ""
|
59
test/manualdocker/02-test-v3-to-v4-migration.sh
Executable file
59
test/manualdocker/02-test-v3-to-v4-migration.sh
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Test migration from v3 to v4
|
||||||
|
# Run this after 01-setup-v3-container.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONTAINER_NAME="nupst-test-v3"
|
||||||
|
|
||||||
|
echo "================================================"
|
||||||
|
echo " NUPST v3 → v4 Migration Test"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if container exists
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
echo "❌ Container ${CONTAINER_NAME} is not running"
|
||||||
|
echo "Run ./01-setup-v3-container.sh first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ Checking current NUPST status..."
|
||||||
|
docker exec ${CONTAINER_NAME} systemctl status nupst --no-pager || true
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "→ Checking current version..."
|
||||||
|
docker exec ${CONTAINER_NAME} nupst --version
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "→ Stopping v3 service..."
|
||||||
|
docker exec ${CONTAINER_NAME} systemctl stop nupst
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "→ Running v4 installation from main branch (should auto-detect v3 and migrate)..."
|
||||||
|
echo " Using: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
|
||||||
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "→ Checking service status after migration..."
|
||||||
|
docker exec ${CONTAINER_NAME} systemctl status nupst --no-pager || true
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "→ Checking new version..."
|
||||||
|
docker exec ${CONTAINER_NAME} nupst --version
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "→ Testing service commands..."
|
||||||
|
docker exec ${CONTAINER_NAME} nupst service status || true
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "================================================"
|
||||||
|
echo " ✓ Migration Test Complete"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
echo "Check logs with:"
|
||||||
|
echo " docker exec ${CONTAINER_NAME} nupst service logs"
|
||||||
|
echo ""
|
28
test/manualdocker/03-cleanup.sh
Executable file
28
test/manualdocker/03-cleanup.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Cleanup test container
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CONTAINER_NAME="nupst-test-v3"
|
||||||
|
|
||||||
|
echo "================================================"
|
||||||
|
echo " Cleanup NUPST Test Container"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||||
|
echo "→ Stopping container..."
|
||||||
|
docker stop ${CONTAINER_NAME} 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "→ Removing container..."
|
||||||
|
docker rm ${CONTAINER_NAME} 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Container ${CONTAINER_NAME} removed"
|
||||||
|
else
|
||||||
|
echo "Container ${CONTAINER_NAME} not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
149
test/manualdocker/README.md
Normal file
149
test/manualdocker/README.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Manual Docker Testing Scripts
|
||||||
|
|
||||||
|
This directory contains scripts for manually testing NUPST installation and migration in Docker containers with systemd support.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker installed and running
|
||||||
|
- Privileged access (for systemd in container)
|
||||||
|
- Linux host (systemd container requirements)
|
||||||
|
|
||||||
|
## Test Scripts
|
||||||
|
|
||||||
|
### 1. `01-setup-v3-container.sh`
|
||||||
|
|
||||||
|
Creates a Docker container with systemd and installs NUPST v3.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- Creates Ubuntu 22.04 container with systemd enabled
|
||||||
|
- Installs NUPST v3 from commit `806f81c6` (last v3 version)
|
||||||
|
- Enables and starts the systemd service
|
||||||
|
- Leaves container running for testing
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
chmod +x 01-setup-v3-container.sh
|
||||||
|
./01-setup-v3-container.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Container name:** `nupst-test-v3`
|
||||||
|
|
||||||
|
### 2. `02-test-v3-to-v4-migration.sh`
|
||||||
|
|
||||||
|
Tests the migration from v3 to v4.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- Checks current v3 installation
|
||||||
|
- Pulls v4 code from `migration/deno-v4` branch
|
||||||
|
- Runs install.sh (should auto-detect and migrate)
|
||||||
|
- Verifies service is running with v4
|
||||||
|
- Tests basic commands
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
chmod +x 02-test-v3-to-v4-migration.sh
|
||||||
|
./02-test-v3-to-v4-migration.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites:** Must run `01-setup-v3-container.sh` first
|
||||||
|
|
||||||
|
### 3. `03-cleanup.sh`
|
||||||
|
|
||||||
|
Removes the test container.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
chmod +x 03-cleanup.sh
|
||||||
|
./03-cleanup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Testing Workflow
|
||||||
|
|
||||||
|
### Full Migration Test
|
||||||
|
|
||||||
|
1. **Set up v3 environment:**
|
||||||
|
```bash
|
||||||
|
./01-setup-v3-container.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify v3 is working:**
|
||||||
|
```bash
|
||||||
|
docker exec nupst-test-v3 nupst --version
|
||||||
|
docker exec nupst-test-v3 systemctl status nupst
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Test migration to v4:**
|
||||||
|
```bash
|
||||||
|
./02-test-v3-to-v4-migration.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Manual verification:**
|
||||||
|
```bash
|
||||||
|
# Enter container
|
||||||
|
docker exec -it nupst-test-v3 bash
|
||||||
|
|
||||||
|
# Inside container:
|
||||||
|
nupst --version # Should show v4.0.0
|
||||||
|
nupst service status # Should show running service
|
||||||
|
cat /etc/nupst/config.json # Config should be preserved
|
||||||
|
systemctl status nupst # Service should be active
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Cleanup:**
|
||||||
|
```bash
|
||||||
|
./03-cleanup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful Docker Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enter container shell
|
||||||
|
docker exec -it nupst-test-v3 bash
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
docker exec nupst-test-v3 systemctl status nupst
|
||||||
|
|
||||||
|
# View service logs
|
||||||
|
docker exec nupst-test-v3 journalctl -u nupst -n 50
|
||||||
|
|
||||||
|
# Check NUPST version
|
||||||
|
docker exec nupst-test-v3 nupst --version
|
||||||
|
|
||||||
|
# Run NUPST commands
|
||||||
|
docker exec nupst-test-v3 nupst service status
|
||||||
|
docker exec nupst-test-v3 nupst ups list
|
||||||
|
|
||||||
|
# Stop container
|
||||||
|
docker stop nupst-test-v3
|
||||||
|
|
||||||
|
# Start container
|
||||||
|
docker start nupst-test-v3
|
||||||
|
|
||||||
|
# Remove container
|
||||||
|
docker rm -f nupst-test-v3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The container runs with `--privileged` flag for systemd support
|
||||||
|
- Container uses Ubuntu 22.04 as base image
|
||||||
|
- v3 installation is from commit `806f81c6a057a2a5da586b96a231d391f12eb1bb`
|
||||||
|
- v4 migration pulls from `migration/deno-v4` branch
|
||||||
|
- All scripts are designed to be idempotent where possible
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container won't start
|
||||||
|
- Ensure Docker daemon is running
|
||||||
|
- Check you have privileged access
|
||||||
|
- Try: `docker logs nupst-test-v3`
|
||||||
|
|
||||||
|
### Systemd not working in container
|
||||||
|
- Requires Linux host (not macOS/Windows)
|
||||||
|
- Needs `--privileged` and cgroup volume mounts
|
||||||
|
- Check: `docker exec nupst-test-v3 systemctl --version`
|
||||||
|
|
||||||
|
### Migration fails
|
||||||
|
- Check logs: `docker exec nupst-test-v3 journalctl -xe`
|
||||||
|
- Verify install.sh ran: `docker exec nupst-test-v3 ls -la /opt/nupst/`
|
||||||
|
- Check service: `docker exec nupst-test-v3 systemctl status nupst`
|
157
test/test.logger.ts
Normal file
157
test/test.logger.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { assert, assertEquals } from 'jsr:@std/assert@^1.0.0';
|
||||||
|
import { Logger } from '../ts/logger.ts';
|
||||||
|
|
||||||
|
// Create a Logger instance for testing
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
|
Deno.test('should create a logger instance', () => {
|
||||||
|
assert(logger instanceof Logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('should log messages with different log levels', () => {
|
||||||
|
// 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
|
||||||
|
assert(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('should create a logbox with title, content, and end', () => {
|
||||||
|
// 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
|
||||||
|
assert(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('should handle width persistence between logbox calls', () => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(errorThrown, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('should use default width when no width is specified', () => {
|
||||||
|
// This should automatically use the default width instead of throwing
|
||||||
|
let errorThrown = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.logBoxLine('This should use default width');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
} catch (_error) {
|
||||||
|
errorThrown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no error was thrown
|
||||||
|
assertEquals(errorThrown, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('should create a complete logbox in one call', () => {
|
||||||
|
// Just ensuring no errors occur
|
||||||
|
logger.logBox('Complete Box', [
|
||||||
|
'Line 1',
|
||||||
|
'Line 2',
|
||||||
|
'Line 3',
|
||||||
|
], 40);
|
||||||
|
|
||||||
|
// Just assert that the test runs without errors
|
||||||
|
assert(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('should handle content that exceeds box width', () => {
|
||||||
|
// 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
|
||||||
|
assert(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('should create dividers with custom characters', () => {
|
||||||
|
// Just ensuring no errors occur
|
||||||
|
logger.logDivider(30);
|
||||||
|
logger.logDivider(20, '*');
|
||||||
|
|
||||||
|
// Just assert that the test runs without errors
|
||||||
|
assert(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('should create divider with default width', () => {
|
||||||
|
// This should use the default width
|
||||||
|
logger.logDivider(undefined, '-');
|
||||||
|
|
||||||
|
// Just assert that the test runs without errors
|
||||||
|
assert(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('Logger Demo', () => {
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Demonstrating default width
|
||||||
|
console.log('\nDefault Width Example:');
|
||||||
|
logger.logBoxLine('This line uses the default width');
|
||||||
|
logger.logBoxLine('Still using default width');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
// Divider example
|
||||||
|
logger.log('\nDivider example:');
|
||||||
|
logger.logDivider(30);
|
||||||
|
logger.logDivider(30, '*');
|
||||||
|
logger.logDivider(undefined, '=');
|
||||||
|
|
||||||
|
assert(true);
|
||||||
|
});
|
233
test/test.showcase.ts
Normal file
233
test/test.showcase.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Showcase test for NUPST CLI outputs
|
||||||
|
* Demonstrates all the beautiful colored output features
|
||||||
|
*
|
||||||
|
* Run with: deno run --allow-all test/showcase.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger, type ITableColumn } from '../ts/logger.ts';
|
||||||
|
import { theme, symbols, getBatteryColor, formatPowerStatus } from '../ts/colors.ts';
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
logger.highlight('NUPST CLI OUTPUT SHOWCASE');
|
||||||
|
logger.dim('Demonstrating beautiful, colored terminal output');
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 1. Basic Logging Methods ===
|
||||||
|
logger.logBoxTitle('Basic Logging Methods', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.log('Normal log message (default color)');
|
||||||
|
logger.success('Success message with ✓ symbol');
|
||||||
|
logger.error('Error message with ✗ symbol');
|
||||||
|
logger.warn('Warning message with ⚠ symbol');
|
||||||
|
logger.info('Info message with ℹ symbol');
|
||||||
|
logger.dim('Dim/secondary text for less important info');
|
||||||
|
logger.highlight('Highlighted/bold text for emphasis');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 2. Colored Boxes ===
|
||||||
|
logger.logBoxTitle('Colored Box Styles', 60);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Boxes can be styled with different colors:');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Success Box (Green)', [
|
||||||
|
'Used for successful operations',
|
||||||
|
'Installation complete, service started, etc.',
|
||||||
|
], 60, 'success');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Error Box (Red)', [
|
||||||
|
'Used for critical errors and failures',
|
||||||
|
'Configuration errors, connection failures, etc.',
|
||||||
|
], 60, 'error');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Warning Box (Yellow)', [
|
||||||
|
'Used for warnings and deprecations',
|
||||||
|
'Old command format, missing config, etc.',
|
||||||
|
], 60, 'warning');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Info Box (Cyan)', [
|
||||||
|
'Used for informational messages',
|
||||||
|
'Version info, update available, etc.',
|
||||||
|
], 60, 'info');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 3. Status Symbols ===
|
||||||
|
logger.logBoxTitle('Status Symbols', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`${symbols.running} Service Running`);
|
||||||
|
logger.logBoxLine(`${symbols.stopped} Service Stopped`);
|
||||||
|
logger.logBoxLine(`${symbols.starting} Service Starting`);
|
||||||
|
logger.logBoxLine(`${symbols.unknown} Status Unknown`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`${symbols.success} Operation Successful`);
|
||||||
|
logger.logBoxLine(`${symbols.error} Operation Failed`);
|
||||||
|
logger.logBoxLine(`${symbols.warning} Warning Condition`);
|
||||||
|
logger.logBoxLine(`${symbols.info} Information`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 4. Battery Level Colors ===
|
||||||
|
logger.logBoxTitle('Battery Level Color Coding', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Battery levels are color-coded:');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(` ${getBatteryColor(85)('85%')} - Good (green, ≥60%)`);
|
||||||
|
logger.logBoxLine(` ${getBatteryColor(45)('45%')} - Medium (yellow, 30-60%)`);
|
||||||
|
logger.logBoxLine(` ${getBatteryColor(15)('15%')} - Critical (red, <30%)`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 5. Power Status Formatting ===
|
||||||
|
logger.logBoxTitle('Power Status Formatting', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Status: ${formatPowerStatus('online')}`);
|
||||||
|
logger.logBoxLine(`Status: ${formatPowerStatus('onBattery')}`);
|
||||||
|
logger.logBoxLine(`Status: ${formatPowerStatus('unknown')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 6. Table Formatting ===
|
||||||
|
const upsColumns: ITableColumn[] = [
|
||||||
|
{ header: 'ID', key: 'id' },
|
||||||
|
{ header: 'Name', key: 'name' },
|
||||||
|
{ header: 'Host', key: 'host' },
|
||||||
|
{ header: 'Status', key: 'status', color: (v) => {
|
||||||
|
if (v.includes('Online')) return theme.success(v);
|
||||||
|
if (v.includes('Battery')) return theme.warning(v);
|
||||||
|
return theme.dim(v);
|
||||||
|
}},
|
||||||
|
{ header: 'Battery', key: 'battery', align: 'right', color: (v) => {
|
||||||
|
const pct = parseInt(v);
|
||||||
|
return getBatteryColor(pct)(v);
|
||||||
|
}},
|
||||||
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const upsData = [
|
||||||
|
{
|
||||||
|
id: 'ups-1',
|
||||||
|
name: 'Main UPS',
|
||||||
|
host: '192.168.1.10',
|
||||||
|
status: 'Online',
|
||||||
|
battery: '95%',
|
||||||
|
runtime: '45 min',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ups-2',
|
||||||
|
name: 'Backup UPS',
|
||||||
|
host: '192.168.1.11',
|
||||||
|
status: 'On Battery',
|
||||||
|
battery: '42%',
|
||||||
|
runtime: '12 min',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ups-3',
|
||||||
|
name: 'Critical UPS',
|
||||||
|
host: '192.168.1.12',
|
||||||
|
status: 'On Battery',
|
||||||
|
battery: '18%',
|
||||||
|
runtime: '5 min',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.logTable(upsColumns, upsData, 'UPS Devices');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 7. Group Table ===
|
||||||
|
const groupColumns: ITableColumn[] = [
|
||||||
|
{ header: 'ID', key: 'id' },
|
||||||
|
{ header: 'Name', key: 'name' },
|
||||||
|
{ header: 'Mode', key: 'mode' },
|
||||||
|
{ header: 'UPS Count', key: 'count', align: 'right' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const groupData = [
|
||||||
|
{ id: 'dc-1', name: 'Data Center 1', mode: 'redundant', count: '3' },
|
||||||
|
{ id: 'office', name: 'Office Servers', mode: 'nonRedundant', count: '2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.logTable(groupColumns, groupData, 'UPS Groups');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 8. Service Status Example ===
|
||||||
|
logger.logBoxTitle('Service Status', 70, 'success');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Status: ${symbols.running} ${theme.statusActive('Active (Running)')}`);
|
||||||
|
logger.logBoxLine(`Enabled: ${symbols.success} ${theme.success('Yes')}`);
|
||||||
|
logger.logBoxLine(`Uptime: 2 days, 5 hours, 23 minutes`);
|
||||||
|
logger.logBoxLine(`PID: ${theme.dim('12345')}`);
|
||||||
|
logger.logBoxLine(`Memory: ${theme.dim('45.2 MB')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 9. Configuration Example ===
|
||||||
|
logger.logBoxTitle('Configuration', 70);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`UPS Devices: ${theme.highlight('3')}`);
|
||||||
|
logger.logBoxLine(`Groups: ${theme.highlight('2')}`);
|
||||||
|
logger.logBoxLine(`Check Interval: ${theme.dim('30 seconds')}`);
|
||||||
|
logger.logBoxLine(`Config File: ${theme.path('/etc/nupst/config.json')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 10. Update Available Example ===
|
||||||
|
logger.logBoxTitle('Update Available', 70, 'warning');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Current Version: ${theme.dim('4.0.1')}`);
|
||||||
|
logger.logBoxLine(`Latest Version: ${theme.highlight('4.0.2')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Run ${theme.command('sudo nupst update')} to update`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 11. Error Example ===
|
||||||
|
logger.logBoxTitle('Error Example', 70, 'error');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`${symbols.error} Failed to connect to UPS at 192.168.1.10`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Possible causes:');
|
||||||
|
logger.logBoxLine(` ${theme.dim('• UPS is offline or unreachable')}`);
|
||||||
|
logger.logBoxLine(` ${theme.dim('• Incorrect SNMP community string')}`);
|
||||||
|
logger.logBoxLine(` ${theme.dim('• Firewall blocking port 161')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Try: ${theme.command('nupst ups test --debug')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === Final Summary ===
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
logger.success('CLI Output Showcase Complete!');
|
||||||
|
logger.dim('All color and formatting features demonstrated');
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
console.log('');
|
361
test/test.ts
361
test/test.ts
@@ -1,330 +1,53 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { assert, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0';
|
||||||
import { NupstSnmp } from '../ts/snmp.js';
|
import { NupstSnmp } from '../ts/snmp/manager.ts';
|
||||||
import type { SnmpConfig, UpsStatus } from '../ts/snmp.js';
|
import type { ISnmpConfig } from '../ts/snmp/types.ts';
|
||||||
import { SnmpEncoder } from '../ts/snmp/encoder.js';
|
|
||||||
import { SnmpPacketCreator } from '../ts/snmp/packet-creator.js';
|
|
||||||
import { SnmpPacketParser } from '../ts/snmp/packet-parser.js';
|
|
||||||
|
|
||||||
import * as qenv from '@push.rocks/qenv';
|
import * as qenv from 'npm:@push.rocks/qenv@^6.0.0';
|
||||||
const testQenv = new qenv.Qenv('./', '.nogit/');
|
const testQenv = new qenv.Qenv('./', '.nogit/');
|
||||||
|
|
||||||
// Create an SNMP instance with debug enabled
|
// Create an SNMP instance with debug enabled
|
||||||
const snmp = new NupstSnmp(true);
|
const snmp = new NupstSnmp(true);
|
||||||
|
|
||||||
// Load the test configuration from .nogit/env.json
|
// 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 () => {
|
Deno.test('should log config', () => {
|
||||||
console.log(testConfig);
|
console.log(testConfigV1);
|
||||||
});
|
assert(true);
|
||||||
|
|
||||||
tap.test('SNMP packet creation and parsing test', async () => {
|
|
||||||
// We'll test the internal methods that are now in separate classes
|
|
||||||
|
|
||||||
// Test OID conversion
|
|
||||||
const oidStr = '1.3.6.1.4.1.3808.1.1.1.4.1.1.0';
|
|
||||||
const oidArray = SnmpEncoder.oidToArray(oidStr);
|
|
||||||
console.log('OID array length:', oidArray.length);
|
|
||||||
console.log('OID array:', oidArray);
|
|
||||||
// The OID has 14 elements after splitting
|
|
||||||
expect(oidArray.length).toEqual(14);
|
|
||||||
expect(oidArray[0]).toEqual(1);
|
|
||||||
expect(oidArray[1]).toEqual(3);
|
|
||||||
|
|
||||||
// Test OID encoding
|
|
||||||
const encodedOid = SnmpEncoder.encodeOID(oidArray);
|
|
||||||
expect(encodedOid).toBeInstanceOf(Buffer);
|
|
||||||
|
|
||||||
// Test SNMP request creation
|
|
||||||
const request = SnmpPacketCreator.createSnmpGetRequest(oidStr, 'public', true);
|
|
||||||
expect(request).toBeInstanceOf(Buffer);
|
|
||||||
expect(request.length).toBeGreaterThan(20);
|
|
||||||
|
|
||||||
// Log the request for debugging
|
|
||||||
console.log('SNMP Request buffer:', request.toString('hex'));
|
|
||||||
|
|
||||||
// Test integer encoding
|
|
||||||
const int = SnmpEncoder.encodeInteger(42);
|
|
||||||
expect(int).toBeInstanceOf(Buffer);
|
|
||||||
expect(int.length).toBeGreaterThanOrEqual(1);
|
|
||||||
|
|
||||||
// Test SNMPv3 engine ID discovery message
|
|
||||||
const discoveryMsg = SnmpPacketCreator.createDiscoveryMessage(testConfig, 1);
|
|
||||||
expect(discoveryMsg).toBeInstanceOf(Buffer);
|
|
||||||
expect(discoveryMsg.length).toBeGreaterThan(20);
|
|
||||||
|
|
||||||
console.log('SNMPv3 Discovery message:', discoveryMsg.toString('hex'));
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('SNMP response parsing simulation', async () => {
|
|
||||||
// Create a simulated SNMP response for parsing
|
|
||||||
|
|
||||||
// Simulate an INTEGER response (battery capacity)
|
|
||||||
const intResponse = Buffer.from([
|
|
||||||
0x30, 0x29, // Sequence, length 41
|
|
||||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
||||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
||||||
0xa2, 0x1c, // GetResponse
|
|
||||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
||||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
||||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
||||||
0x30, 0x11, // Sequence (varbinds)
|
|
||||||
0x30, 0x0f, // Sequence (varbind)
|
|
||||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example)
|
|
||||||
0x02, 0x01, 0x64 // Integer (value), value 100 (100%)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Simulate a Gauge32 response (battery capacity)
|
|
||||||
const gauge32Response = Buffer.from([
|
|
||||||
0x30, 0x29, // Sequence, length 41
|
|
||||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
||||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
||||||
0xa2, 0x1c, // GetResponse
|
|
||||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
||||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
||||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
||||||
0x30, 0x11, // Sequence (varbinds)
|
|
||||||
0x30, 0x0f, // Sequence (varbind)
|
|
||||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example)
|
|
||||||
0x42, 0x01, 0x64 // Gauge32 (value), value 100 (100%)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Simulate a TimeTicks response (battery runtime)
|
|
||||||
const timeTicksResponse = Buffer.from([
|
|
||||||
0x30, 0x29, // Sequence, length 41
|
|
||||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
||||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
||||||
0xa2, 0x1c, // GetResponse
|
|
||||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
||||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
||||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
||||||
0x30, 0x11, // Sequence (varbinds)
|
|
||||||
0x30, 0x0f, // Sequence (varbind)
|
|
||||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example)
|
|
||||||
0x43, 0x01, 0x0f // TimeTicks (value), value 15 (0.15 seconds or 15/100 seconds)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Test parsing INTEGER response
|
|
||||||
const intValue = SnmpPacketParser.parseSnmpResponse(intResponse, testConfig, true);
|
|
||||||
console.log('Parsed INTEGER value:', intValue);
|
|
||||||
expect(intValue).toEqual(100);
|
|
||||||
|
|
||||||
// Test parsing Gauge32 response
|
|
||||||
const gauge32Value = SnmpPacketParser.parseSnmpResponse(gauge32Response, testConfig, true);
|
|
||||||
console.log('Parsed Gauge32 value:', gauge32Value);
|
|
||||||
expect(gauge32Value).toEqual(100);
|
|
||||||
|
|
||||||
// Test parsing TimeTicks response
|
|
||||||
const timeTicksValue = SnmpPacketParser.parseSnmpResponse(timeTicksResponse, testConfig, true);
|
|
||||||
console.log('Parsed TimeTicks value:', timeTicksValue);
|
|
||||||
expect(timeTicksValue).toEqual(15);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CyberPower TimeTicks conversion', async () => {
|
|
||||||
// Test the conversion of TimeTicks to minutes for CyberPower UPS
|
|
||||||
|
|
||||||
// Set up a config for CyberPower
|
|
||||||
const cyberPowerConfig: SnmpConfig = {
|
|
||||||
...testConfig,
|
|
||||||
upsModel: 'cyberpower'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a simulated TimeTicks response with a value of 104 (104/100 seconds)
|
|
||||||
const ticksResponse = Buffer.from([
|
|
||||||
0x30, 0x29, // Sequence
|
|
||||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
||||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
||||||
0xa2, 0x1c, // GetResponse
|
|
||||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
||||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
||||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
||||||
0x30, 0x11, // Sequence (varbinds)
|
|
||||||
0x30, 0x0f, // Sequence (varbind)
|
|
||||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime)
|
|
||||||
0x43, 0x01, 0x68 // TimeTicks (value), value 104 (104/100 seconds)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Mock the getUpsStatus function to test our TimeTicks conversion logic
|
|
||||||
const mockGetUpsStatus = async () => {
|
|
||||||
// Parse the TimeTicks value from the response
|
|
||||||
const runtime = SnmpPacketParser.parseSnmpResponse(ticksResponse, testConfig, true);
|
|
||||||
console.log('Raw runtime value:', runtime);
|
|
||||||
|
|
||||||
// Create a sample UPS status result
|
|
||||||
const result = {
|
|
||||||
powerStatus: 'onBattery',
|
|
||||||
batteryCapacity: 100,
|
|
||||||
batteryRuntime: 0,
|
|
||||||
raw: {
|
|
||||||
powerStatus: 2,
|
|
||||||
batteryCapacity: 100,
|
|
||||||
batteryRuntime: runtime,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert TimeTicks to minutes for CyberPower
|
|
||||||
if (cyberPowerConfig.upsModel === 'cyberpower' && runtime > 0) {
|
|
||||||
result.batteryRuntime = Math.floor(runtime / 6000);
|
|
||||||
console.log(`Converting CyberPower runtime from ${runtime} ticks to ${result.batteryRuntime} minutes`);
|
|
||||||
} else {
|
|
||||||
result.batteryRuntime = runtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call our mock function
|
|
||||||
const status = await mockGetUpsStatus();
|
|
||||||
|
|
||||||
// Assert the conversion worked correctly
|
|
||||||
console.log('Final status object:', status);
|
|
||||||
expect(status.batteryRuntime).toEqual(0); // 104 ticks / 6000 = 0.0173... rounds to 0 minutes
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('Simulate fully charged online UPS', async () => {
|
|
||||||
// Test a realistic scenario of an online UPS with high battery capacity and ~30 mins runtime
|
|
||||||
|
|
||||||
// Create simulated responses for power status (online), battery capacity (95%), runtime (30 min)
|
|
||||||
|
|
||||||
// Power Status = 2 (online for CyberPower)
|
|
||||||
const powerStatusResponse = Buffer.from([
|
|
||||||
0x30, 0x29, // Sequence
|
|
||||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
||||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
||||||
0xa2, 0x1c, // GetResponse
|
|
||||||
0x02, 0x01, 0x01, // Integer (request ID), value 1
|
|
||||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
||||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
||||||
0x30, 0x11, // Sequence (varbinds)
|
|
||||||
0x30, 0x0f, // Sequence (varbind)
|
|
||||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x01, 0x01, 0x00, // OID (power status)
|
|
||||||
0x02, 0x01, 0x02 // Integer (value), value 2 (online)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Battery Capacity = 95% (as Gauge32)
|
|
||||||
const batteryCapacityResponse = Buffer.from([
|
|
||||||
0x30, 0x29, // Sequence
|
|
||||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
||||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
||||||
0xa2, 0x1c, // GetResponse
|
|
||||||
0x02, 0x01, 0x02, // Integer (request ID), value 2
|
|
||||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
||||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
||||||
0x30, 0x11, // Sequence (varbinds)
|
|
||||||
0x30, 0x0f, // Sequence (varbind)
|
|
||||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x01, 0x00, // OID (battery capacity)
|
|
||||||
0x42, 0x01, 0x5F // Gauge32 (value), value 95 (95%)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Battery Runtime = 30 minutes (as TimeTicks)
|
|
||||||
// 30 minutes = 1800 seconds = 180000 ticks (in 1/100 seconds)
|
|
||||||
const batteryRuntimeResponse = Buffer.from([
|
|
||||||
0x30, 0x2c, // Sequence
|
|
||||||
0x02, 0x01, 0x00, // Integer (version), value 0
|
|
||||||
0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public"
|
|
||||||
0xa2, 0x1f, // GetResponse
|
|
||||||
0x02, 0x01, 0x03, // Integer (request ID), value 3
|
|
||||||
0x02, 0x01, 0x00, // Integer (error status), value 0
|
|
||||||
0x02, 0x01, 0x00, // Integer (error index), value 0
|
|
||||||
0x30, 0x14, // Sequence (varbinds)
|
|
||||||
0x30, 0x12, // Sequence (varbind)
|
|
||||||
0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime)
|
|
||||||
0x43, 0x04, 0x00, 0x02, 0xBF, 0x20 // TimeTicks (value), value 180000 (1800 seconds = 30 minutes)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Mock the getUpsStatus function to test with our simulated data
|
|
||||||
const mockGetUpsStatus = async () => {
|
|
||||||
console.log('Simulating UPS status request with synthetic data');
|
|
||||||
|
|
||||||
// Create a config that specifies this is a CyberPower UPS
|
|
||||||
const upsConfig: SnmpConfig = {
|
|
||||||
host: '192.168.1.1',
|
|
||||||
port: 161,
|
|
||||||
version: 1,
|
|
||||||
community: 'public',
|
|
||||||
timeout: 5000,
|
|
||||||
upsModel: 'cyberpower',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse each simulated response
|
|
||||||
const powerStatus = SnmpPacketParser.parseSnmpResponse(powerStatusResponse, upsConfig, true);
|
|
||||||
console.log('Power status value:', powerStatus);
|
|
||||||
|
|
||||||
const batteryCapacity = SnmpPacketParser.parseSnmpResponse(batteryCapacityResponse, upsConfig, true);
|
|
||||||
console.log('Battery capacity value:', batteryCapacity);
|
|
||||||
|
|
||||||
const batteryRuntime = SnmpPacketParser.parseSnmpResponse(batteryRuntimeResponse, upsConfig, true);
|
|
||||||
console.log('Battery runtime value:', batteryRuntime);
|
|
||||||
|
|
||||||
// Convert TimeTicks to minutes for CyberPower UPSes
|
|
||||||
const runtimeMinutes = Math.floor(batteryRuntime / 6000);
|
|
||||||
console.log(`Converting ${batteryRuntime} ticks to ${runtimeMinutes} minutes`);
|
|
||||||
|
|
||||||
// Interpret power status for CyberPower
|
|
||||||
// CyberPower: 2=online, 3=on battery
|
|
||||||
let powerStatusText: 'online' | 'onBattery' | 'unknown' = 'unknown';
|
|
||||||
if (powerStatus === 2) {
|
|
||||||
powerStatusText = 'online';
|
|
||||||
} else if (powerStatus === 3) {
|
|
||||||
powerStatusText = 'onBattery';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the status result
|
|
||||||
const result: UpsStatus = {
|
|
||||||
powerStatus: powerStatusText,
|
|
||||||
batteryCapacity: batteryCapacity,
|
|
||||||
batteryRuntime: runtimeMinutes,
|
|
||||||
raw: {
|
|
||||||
powerStatus,
|
|
||||||
batteryCapacity,
|
|
||||||
batteryRuntime,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call our mock function
|
|
||||||
const status = await mockGetUpsStatus();
|
|
||||||
|
|
||||||
// Assert that the values match our expectations
|
|
||||||
console.log('UPS Status Result:', status);
|
|
||||||
expect(status.powerStatus).toEqual('online');
|
|
||||||
expect(status.batteryCapacity).toEqual(95);
|
|
||||||
expect(status.batteryRuntime).toEqual(30);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test with real UPS using the configuration from .nogit/env.json
|
// Test with real UPS using the configuration from .nogit/env.json
|
||||||
tap.test('Real UPS test', async () => {
|
Deno.test('Real UPS test v1', async () => {
|
||||||
try {
|
try {
|
||||||
console.log('Testing with real UPS configuration...');
|
console.log('Testing with real UPS configuration...');
|
||||||
|
|
||||||
// Extract the correct SNMP config from the test configuration
|
// Extract the correct SNMP config from the test configuration
|
||||||
const snmpConfig = testConfig.snmp;
|
const snmpConfig = testConfigV1.snmp as ISnmpConfig;
|
||||||
console.log('SNMP Config:');
|
console.log('SNMP Config:');
|
||||||
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
||||||
console.log(` Version: SNMPv${snmpConfig.version}`);
|
console.log(` Version: SNMPv${snmpConfig.version}`);
|
||||||
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
||||||
|
|
||||||
// Use a short timeout for testing
|
// Use a short timeout for testing
|
||||||
const testSnmpConfig = {
|
const testSnmpConfig = {
|
||||||
...snmpConfig,
|
...snmpConfig,
|
||||||
timeout: Math.min(snmpConfig.timeout, 10000) // Use at most 10 seconds for testing
|
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to get the UPS status
|
// Try to get the UPS status
|
||||||
const status = await snmp.getUpsStatus(testSnmpConfig);
|
const status = await snmp.getUpsStatus(testSnmpConfig);
|
||||||
|
|
||||||
console.log('UPS Status:');
|
console.log('UPS Status:');
|
||||||
console.log(` Power Status: ${status.powerStatus}`);
|
console.log(` Power Status: ${status.powerStatus}`);
|
||||||
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
||||||
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||||
|
|
||||||
// Just make sure we got valid data types back
|
// Just make sure we got valid data types back
|
||||||
expect(status).toBeTruthy();
|
assertExists(status);
|
||||||
expect(['online', 'onBattery', 'unknown']).toContain(status.powerStatus);
|
assert(['online', 'onBattery', 'unknown'].includes(status.powerStatus));
|
||||||
expect(typeof status.batteryCapacity).toEqual('number');
|
assertEquals(typeof status.batteryCapacity, 'number');
|
||||||
expect(typeof status.batteryRuntime).toEqual('number');
|
assertEquals(typeof status.batteryRuntime, 'number');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Real UPS test failed:', error);
|
console.log('Real UPS test failed:', error);
|
||||||
// Skip the test if we can't connect to the real UPS
|
// Skip the test if we can't connect to the real UPS
|
||||||
@@ -332,5 +55,39 @@ tap.test('Real UPS test', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export the default tap object
|
Deno.test('Real UPS test v3', async () => {
|
||||||
export default tap.start();
|
try {
|
||||||
|
console.log('Testing with real UPS configuration...');
|
||||||
|
|
||||||
|
// Extract the correct SNMP config from the test configuration
|
||||||
|
const snmpConfig = testConfigV3.snmp as ISnmpConfig;
|
||||||
|
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
|
||||||
|
assertExists(status);
|
||||||
|
assert(['online', 'onBattery', 'unknown'].includes(status.powerStatus));
|
||||||
|
assertEquals(typeof status.batteryCapacity, 'number');
|
||||||
|
assertEquals(typeof status.batteryRuntime, '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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* autocreated commitinfo by @push.rocks/commitinfo
|
* commitinfo - reads version from deno.json
|
||||||
*/
|
*/
|
||||||
|
import denoConfig from '../deno.json' with { type: 'json' };
|
||||||
|
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/nupst',
|
name: denoConfig.name,
|
||||||
version: '1.8.0',
|
version: denoConfig.version,
|
||||||
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
|
description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)',
|
||||||
}
|
};
|
||||||
|
596
ts/cli/group-handler.ts
Normal file
596
ts/cli/group-handler.ts
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
import process from 'node:process';
|
||||||
|
import { Nupst } from '../nupst.ts';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
import * as helpers from '../helpers/index.ts';
|
||||||
|
import { type IGroupConfig } from '../daemon.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling group-related CLI commands
|
||||||
|
* Provides interface for managing UPS groups
|
||||||
|
*/
|
||||||
|
export class GroupHandler {
|
||||||
|
private readonly nupst: Nupst;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Group handler
|
||||||
|
* @param nupst Reference to the main Nupst instance
|
||||||
|
*/
|
||||||
|
constructor(nupst: Nupst) {
|
||||||
|
this.nupst = nupst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all UPS groups
|
||||||
|
*/
|
||||||
|
public async list(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Try to load configuration
|
||||||
|
try {
|
||||||
|
await this.nupst.getDaemon().loadConfig();
|
||||||
|
} catch (error) {
|
||||||
|
const errorBoxWidth = 45;
|
||||||
|
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
||||||
|
logger.logBoxLine('No configuration found.');
|
||||||
|
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
||||||
|
logger.logBoxEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current configuration
|
||||||
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
|
// Check if multi-UPS config
|
||||||
|
if (!config.groups || !Array.isArray(config.groups)) {
|
||||||
|
// Legacy or missing groups configuration
|
||||||
|
const boxWidth = 45;
|
||||||
|
logger.logBoxTitle('UPS Groups', boxWidth);
|
||||||
|
logger.logBoxLine('No groups configured.');
|
||||||
|
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display group list
|
||||||
|
const boxWidth = 60;
|
||||||
|
logger.logBoxTitle('UPS Groups', boxWidth);
|
||||||
|
|
||||||
|
if (config.groups.length === 0) {
|
||||||
|
logger.logBoxLine('No UPS groups configured.');
|
||||||
|
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
||||||
|
} else {
|
||||||
|
logger.logBoxLine(`Found ${config.groups.length} group(s)`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('ID | Name | Mode | UPS Devices');
|
||||||
|
logger.logBoxLine('-----------+----------------------+--------------+----------------');
|
||||||
|
|
||||||
|
for (const group of config.groups) {
|
||||||
|
const id = group.id.padEnd(10, ' ').substring(0, 10);
|
||||||
|
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
|
||||||
|
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
|
||||||
|
|
||||||
|
// Count UPS devices in this group
|
||||||
|
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
|
||||||
|
const upsCount = upsInGroup.length;
|
||||||
|
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
|
||||||
|
|
||||||
|
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.logBoxEnd();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new UPS group
|
||||||
|
*/
|
||||||
|
public async add(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Import readline module for user input
|
||||||
|
const readline = await import('node:readline');
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to prompt for input
|
||||||
|
const prompt = (question: string): Promise<string> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer: string) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to load configuration
|
||||||
|
try {
|
||||||
|
await this.nupst.getDaemon().loadConfig();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current configuration
|
||||||
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
|
// Initialize groups array if not exists
|
||||||
|
if (!config.groups) {
|
||||||
|
config.groups = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upsDevices is initialized
|
||||||
|
if (!config.upsDevices) {
|
||||||
|
config.upsDevices = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('\nNUPST Add Group');
|
||||||
|
logger.log('==============\n');
|
||||||
|
logger.log('This will guide you through creating a new UPS group.\n');
|
||||||
|
|
||||||
|
// Generate a new unique group ID
|
||||||
|
const groupId = helpers.shortId();
|
||||||
|
|
||||||
|
// Get group name
|
||||||
|
const name = await prompt('Group Name: ');
|
||||||
|
|
||||||
|
// Get group mode
|
||||||
|
const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: ');
|
||||||
|
const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
||||||
|
|
||||||
|
// Get optional description
|
||||||
|
const description = await prompt('Group Description (optional): ');
|
||||||
|
|
||||||
|
// Create the new group
|
||||||
|
const newGroup: IGroupConfig = {
|
||||||
|
id: groupId,
|
||||||
|
name: name || `Group-${groupId}`,
|
||||||
|
mode,
|
||||||
|
description: description || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the group to the configuration
|
||||||
|
config.groups.push(newGroup);
|
||||||
|
|
||||||
|
// Save the configuration
|
||||||
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
|
// Display summary
|
||||||
|
const boxWidth = 45;
|
||||||
|
logger.logBoxTitle('Group Created', boxWidth);
|
||||||
|
logger.logBoxLine(`ID: ${newGroup.id}`);
|
||||||
|
logger.logBoxLine(`Name: ${newGroup.name}`);
|
||||||
|
logger.logBoxLine(`Mode: ${newGroup.mode}`);
|
||||||
|
if (newGroup.description) {
|
||||||
|
logger.logBoxLine(`Description: ${newGroup.description}`);
|
||||||
|
}
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
// Check if there are UPS devices to assign to this group
|
||||||
|
if (config.upsDevices.length > 0) {
|
||||||
|
const assignUps = await prompt(
|
||||||
|
'Would you like to assign UPS devices to this group now? (y/N): ',
|
||||||
|
);
|
||||||
|
if (assignUps.toLowerCase() === 'y') {
|
||||||
|
await this.assignUpsToGroup(newGroup.id, config, prompt);
|
||||||
|
|
||||||
|
// Save again after assigning UPS devices
|
||||||
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if service is running and restart it if needed
|
||||||
|
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||||
|
|
||||||
|
logger.log('\nGroup setup complete!');
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit an existing UPS group
|
||||||
|
* @param groupId ID of the group to edit
|
||||||
|
*/
|
||||||
|
public async edit(groupId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Import readline module for user input
|
||||||
|
const readline = await import('node:readline');
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to prompt for input
|
||||||
|
const prompt = (question: string): Promise<string> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer: string) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to load configuration
|
||||||
|
try {
|
||||||
|
await this.nupst.getDaemon().loadConfig();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current configuration
|
||||||
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
|
// Check if groups are initialized
|
||||||
|
if (!config.groups || !Array.isArray(config.groups)) {
|
||||||
|
logger.error(
|
||||||
|
'No groups configured. Please run "nupst group add" first to create a group.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the group to edit
|
||||||
|
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
|
||||||
|
if (groupIndex === -1) {
|
||||||
|
logger.error(`Group with ID "${groupId}" not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = config.groups[groupIndex];
|
||||||
|
|
||||||
|
logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`);
|
||||||
|
logger.log('==============================================\n');
|
||||||
|
|
||||||
|
// Edit group name
|
||||||
|
const newName = await prompt(`Group Name [${group.name}]: `);
|
||||||
|
if (newName.trim()) {
|
||||||
|
group.name = newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit group mode
|
||||||
|
const currentMode = group.mode || 'redundant';
|
||||||
|
const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `);
|
||||||
|
if (modeInput.trim()) {
|
||||||
|
group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit description
|
||||||
|
const currentDesc = group.description || '';
|
||||||
|
const newDesc = await prompt(`Group Description [${currentDesc}]: `);
|
||||||
|
if (newDesc.trim() || newDesc === '') {
|
||||||
|
group.description = newDesc.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the group in the configuration
|
||||||
|
config.groups[groupIndex] = group;
|
||||||
|
|
||||||
|
// Save the configuration
|
||||||
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
|
// Display summary
|
||||||
|
const boxWidth = 45;
|
||||||
|
logger.logBoxTitle('Group Updated', boxWidth);
|
||||||
|
logger.logBoxLine(`ID: ${group.id}`);
|
||||||
|
logger.logBoxLine(`Name: ${group.name}`);
|
||||||
|
logger.logBoxLine(`Mode: ${group.mode}`);
|
||||||
|
if (group.description) {
|
||||||
|
logger.logBoxLine(`Description: ${group.description}`);
|
||||||
|
}
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
// Edit UPS assignments if requested
|
||||||
|
const editAssignments = await prompt(
|
||||||
|
'Would you like to edit UPS assignments for this group? (y/N): ',
|
||||||
|
);
|
||||||
|
if (editAssignments.toLowerCase() === 'y') {
|
||||||
|
await this.assignUpsToGroup(group.id, config, prompt);
|
||||||
|
|
||||||
|
// Save again after editing assignments
|
||||||
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if service is running and restart it if needed
|
||||||
|
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||||
|
|
||||||
|
logger.log('\nGroup edit complete!');
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing UPS group
|
||||||
|
* @param groupId ID of the group to delete
|
||||||
|
*/
|
||||||
|
public async remove(groupId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Try to load configuration
|
||||||
|
try {
|
||||||
|
await this.nupst.getDaemon().loadConfig();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current configuration
|
||||||
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
|
// Check if groups are initialized
|
||||||
|
if (!config.groups || !Array.isArray(config.groups)) {
|
||||||
|
logger.error('No groups configured.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the group to delete
|
||||||
|
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
|
||||||
|
if (groupIndex === -1) {
|
||||||
|
logger.error(`Group with ID "${groupId}" not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupToDelete = config.groups[groupIndex];
|
||||||
|
|
||||||
|
// Get confirmation before deleting
|
||||||
|
const readline = await import('node:readline');
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirm = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(
|
||||||
|
`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `,
|
||||||
|
(answer) => {
|
||||||
|
resolve(answer.toLowerCase());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
|
|
||||||
|
if (confirm !== 'y' && confirm !== 'yes') {
|
||||||
|
logger.log('Deletion cancelled.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove this group from all UPS device group assignments
|
||||||
|
if (config.upsDevices && Array.isArray(config.upsDevices)) {
|
||||||
|
for (const ups of config.upsDevices) {
|
||||||
|
const groupIndex = ups.groups.indexOf(groupId);
|
||||||
|
if (groupIndex !== -1) {
|
||||||
|
ups.groups.splice(groupIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the group from the array
|
||||||
|
config.groups.splice(groupIndex, 1);
|
||||||
|
|
||||||
|
// Save the configuration
|
||||||
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
|
logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`);
|
||||||
|
|
||||||
|
// Check if service is running and restart it if needed
|
||||||
|
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to delete group: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign UPS devices to groups
|
||||||
|
* @param ups UPS configuration to update
|
||||||
|
* @param groups Available groups
|
||||||
|
* @param prompt Function to prompt for user input
|
||||||
|
*/
|
||||||
|
public async assignUpsToGroups(
|
||||||
|
ups: any,
|
||||||
|
groups: any[],
|
||||||
|
prompt: (question: string) => Promise<string>,
|
||||||
|
): Promise<void> {
|
||||||
|
// Initialize groups array if it doesn't exist
|
||||||
|
if (!ups.groups) {
|
||||||
|
ups.groups = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show current group assignments
|
||||||
|
logger.log('\nCurrent Group Assignments:');
|
||||||
|
if (ups.groups && ups.groups.length > 0) {
|
||||||
|
for (const groupId of ups.groups) {
|
||||||
|
const group = groups.find((g) => g.id === groupId);
|
||||||
|
if (group) {
|
||||||
|
logger.log(`- ${group.name} (${group.id})`);
|
||||||
|
} else {
|
||||||
|
logger.log(`- Unknown group (${groupId})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.log('- None');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show available groups
|
||||||
|
logger.log('\nAvailable Groups:');
|
||||||
|
if (groups.length === 0) {
|
||||||
|
logger.log('- No groups available. Use "nupst group add" to create groups.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < groups.length; i++) {
|
||||||
|
const group = groups[i];
|
||||||
|
const assigned = ups.groups && ups.groups.includes(group.id);
|
||||||
|
logger.log(
|
||||||
|
`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt for group selection
|
||||||
|
const selection = await prompt(
|
||||||
|
'\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selection.toLowerCase() === 'clear') {
|
||||||
|
// Clear all group assignments
|
||||||
|
ups.groups = [];
|
||||||
|
logger.log('All group assignments cleared.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selection.trim()) {
|
||||||
|
// No change if empty input
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process selections
|
||||||
|
const selections = selection.split(',').map((s) => s.trim());
|
||||||
|
|
||||||
|
for (const sel of selections) {
|
||||||
|
const index = parseInt(sel, 10) - 1;
|
||||||
|
if (isNaN(index) || index < 0 || index >= groups.length) {
|
||||||
|
logger.error(`Invalid selection: ${sel}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groups[index];
|
||||||
|
|
||||||
|
// Initialize groups array if needed (should already be done above)
|
||||||
|
if (!ups.groups) {
|
||||||
|
ups.groups = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle assignment
|
||||||
|
const groupIndex = ups.groups.indexOf(group.id);
|
||||||
|
if (groupIndex === -1) {
|
||||||
|
// Add to group
|
||||||
|
ups.groups.push(group.id);
|
||||||
|
logger.log(`Added to group: ${group.name} (${group.id})`);
|
||||||
|
} else {
|
||||||
|
// Remove from group
|
||||||
|
ups.groups.splice(groupIndex, 1);
|
||||||
|
logger.log(`Removed from group: ${group.name} (${group.id})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign UPS devices to a specific group
|
||||||
|
* @param groupId Group ID to assign UPS devices to
|
||||||
|
* @param config Full configuration
|
||||||
|
* @param prompt Function to prompt for user input
|
||||||
|
*/
|
||||||
|
public async assignUpsToGroup(
|
||||||
|
groupId: string,
|
||||||
|
config: any,
|
||||||
|
prompt: (question: string) => Promise<string>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!config.upsDevices || config.upsDevices.length === 0) {
|
||||||
|
logger.log('No UPS devices available. Use "nupst add" to add UPS devices.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = config.groups.find((g: { id: string }) => g.id === groupId);
|
||||||
|
if (!group) {
|
||||||
|
logger.error(`Group with ID "${groupId}" not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show current assignments
|
||||||
|
logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`);
|
||||||
|
const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) =>
|
||||||
|
ups.groups && ups.groups.includes(groupId)
|
||||||
|
);
|
||||||
|
if (upsInGroup.length === 0) {
|
||||||
|
logger.log('- None');
|
||||||
|
} else {
|
||||||
|
for (const ups of upsInGroup) {
|
||||||
|
logger.log(`- ${ups.name} (${ups.id})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show all UPS devices
|
||||||
|
logger.log('\nAvailable UPS devices:');
|
||||||
|
for (let i = 0; i < config.upsDevices.length; i++) {
|
||||||
|
const ups = config.upsDevices[i];
|
||||||
|
const assigned = ups.groups && ups.groups.includes(groupId);
|
||||||
|
logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt for UPS selection
|
||||||
|
const selection = await prompt(
|
||||||
|
'\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selection.toLowerCase() === 'clear') {
|
||||||
|
// Clear all UPS from this group
|
||||||
|
for (const ups of config.upsDevices) {
|
||||||
|
if (ups.groups) {
|
||||||
|
const groupIndex = ups.groups.indexOf(groupId);
|
||||||
|
if (groupIndex !== -1) {
|
||||||
|
ups.groups.splice(groupIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.log(`All UPS devices removed from group "${group.name}".`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selection.trim()) {
|
||||||
|
// No change if empty input
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process selections
|
||||||
|
const selections = selection.split(',').map((s) => s.trim());
|
||||||
|
|
||||||
|
for (const sel of selections) {
|
||||||
|
const index = parseInt(sel, 10) - 1;
|
||||||
|
if (isNaN(index) || index < 0 || index >= config.upsDevices.length) {
|
||||||
|
logger.error(`Invalid selection: ${sel}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ups = config.upsDevices[index];
|
||||||
|
|
||||||
|
// Initialize groups array if needed
|
||||||
|
if (!ups.groups) {
|
||||||
|
ups.groups = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle assignment
|
||||||
|
const groupIndex = ups.groups.indexOf(groupId);
|
||||||
|
if (groupIndex === -1) {
|
||||||
|
// Add to group
|
||||||
|
ups.groups.push(groupId);
|
||||||
|
logger.log(`Added "${ups.name}" to group "${group.name}"`);
|
||||||
|
} else {
|
||||||
|
// Remove from group
|
||||||
|
ups.groups.splice(groupIndex, 1);
|
||||||
|
logger.log(`Removed "${ups.name}" from group "${group.name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
302
ts/cli/service-handler.ts
Normal file
302
ts/cli/service-handler.ts
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import process from 'node:process';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { Nupst } from '../nupst.ts';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling service-related CLI commands
|
||||||
|
* Provides interface for managing systemd service
|
||||||
|
*/
|
||||||
|
export class ServiceHandler {
|
||||||
|
private readonly nupst: Nupst;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Service handler
|
||||||
|
* @param nupst Reference to the main Nupst instance
|
||||||
|
*/
|
||||||
|
constructor(nupst: Nupst) {
|
||||||
|
this.nupst = nupst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the service (requires root)
|
||||||
|
*/
|
||||||
|
public async enable(): Promise<void> {
|
||||||
|
this.checkRootAccess('This command must be run as root.');
|
||||||
|
await this.nupst.getSystemd().install();
|
||||||
|
logger.log('NUPST service has been installed. Use "nupst start" to start the service.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the daemon directly
|
||||||
|
* @param debugMode Whether to enable debug mode
|
||||||
|
*/
|
||||||
|
public async daemonStart(debugMode: boolean = false): Promise<void> {
|
||||||
|
logger.log('Starting NUPST daemon...');
|
||||||
|
try {
|
||||||
|
// Enable debug mode for SNMP if requested
|
||||||
|
if (debugMode) {
|
||||||
|
this.nupst.getSnmp().enableDebug();
|
||||||
|
logger.log('SNMP debug mode enabled');
|
||||||
|
}
|
||||||
|
await this.nupst.getDaemon().start();
|
||||||
|
} catch (error) {
|
||||||
|
// Error is already logged and process.exit is called in daemon.start()
|
||||||
|
// No need to handle it here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show logs of the systemd service
|
||||||
|
*/
|
||||||
|
public async logs(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Use exec with spawn to properly follow logs in real-time
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
logger.log('Tailing nupst service logs (Ctrl+C to exit)...\n');
|
||||||
|
|
||||||
|
const journalctl = spawn('journalctl', ['-u', 'nupst.service', '-n', '50', '-f'], {
|
||||||
|
stdio: ['ignore', 'inherit', 'inherit'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Forward signals to child process
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
journalctl.kill('SIGINT');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for process to exit
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
journalctl.on('exit', () => resolve());
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to retrieve logs: ${error}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the systemd service
|
||||||
|
*/
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
await this.nupst.getSystemd().stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the systemd service
|
||||||
|
*/
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.nupst.getSystemd().start();
|
||||||
|
} catch (error) {
|
||||||
|
// Error will be displayed by systemd.start()
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show status of the systemd service and UPS
|
||||||
|
*/
|
||||||
|
public async status(): Promise<void> {
|
||||||
|
// Extract debug options from args array
|
||||||
|
const debugOptions = this.extractDebugOptions(process.argv);
|
||||||
|
await this.nupst.getSystemd().getStatus(debugOptions.debugMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the service (requires root)
|
||||||
|
*/
|
||||||
|
public async disable(): Promise<void> {
|
||||||
|
this.checkRootAccess('This command must be run as root.');
|
||||||
|
await this.nupst.getSystemd().disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user has root access
|
||||||
|
* @param errorMessage Error message to display if not root
|
||||||
|
*/
|
||||||
|
private checkRootAccess(errorMessage: string): void {
|
||||||
|
if (process.getuid && process.getuid() !== 0) {
|
||||||
|
logger.error(errorMessage);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update NUPST from repository and refresh systemd service
|
||||||
|
*/
|
||||||
|
public async update(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if running as root
|
||||||
|
this.checkRootAccess(
|
||||||
|
'This command must be run as root to update NUPST.',
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
logger.info('Checking for updates...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get current version
|
||||||
|
const currentVersion = this.nupst.getVersion();
|
||||||
|
|
||||||
|
// Fetch latest version from Gitea API
|
||||||
|
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest';
|
||||||
|
const response = execSync(`curl -sSL ${apiUrl}`).toString();
|
||||||
|
const release = JSON.parse(response);
|
||||||
|
const latestVersion = release.tag_name; // e.g., "v4.0.7"
|
||||||
|
|
||||||
|
// Normalize versions for comparison (ensure both have "v" prefix)
|
||||||
|
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
|
||||||
|
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
|
||||||
|
|
||||||
|
logger.dim(`Current version: ${normalizedCurrent}`);
|
||||||
|
logger.dim(`Latest version: ${normalizedLatest}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Compare normalized versions
|
||||||
|
if (normalizedCurrent === normalizedLatest) {
|
||||||
|
logger.success('Already up to date!');
|
||||||
|
console.log('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`New version available: ${latestVersion}`);
|
||||||
|
logger.dim('Downloading and installing...');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Download and run the install script
|
||||||
|
// This handles everything: download binary, stop service, replace, restart
|
||||||
|
const installUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh';
|
||||||
|
|
||||||
|
execSync(`curl -sSL ${installUrl} | bash`, {
|
||||||
|
stdio: 'inherit', // Show install script output to user
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
logger.success(`Updated to ${latestVersion}`);
|
||||||
|
console.log('');
|
||||||
|
} catch (error) {
|
||||||
|
console.log('');
|
||||||
|
logger.error('Update failed');
|
||||||
|
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
console.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completely uninstall NUPST from the system
|
||||||
|
*/
|
||||||
|
public async uninstall(): Promise<void> {
|
||||||
|
// Check if running as root
|
||||||
|
this.checkRootAccess('This command must be run as root.');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Import readline module for user input
|
||||||
|
const readline = await import('readline');
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to prompt for input
|
||||||
|
const prompt = (question: string): Promise<string> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer: string) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.highlight('NUPST Uninstaller');
|
||||||
|
logger.dim('===============');
|
||||||
|
logger.log('This will completely remove NUPST from your system.');
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
// Ask about removing configuration
|
||||||
|
const removeConfig = await prompt(
|
||||||
|
'Do you want to remove the NUPST configuration files? (y/N): ',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the uninstall.sh script location
|
||||||
|
let uninstallScriptPath: string;
|
||||||
|
|
||||||
|
// Try to determine script location based on executable path
|
||||||
|
try {
|
||||||
|
// For ESM, we can use import.meta.url, but since we might be in CJS
|
||||||
|
// we'll use a more reliable approach based on process.argv[1]
|
||||||
|
const binPath = process.argv[1];
|
||||||
|
const { dirname, join } = await import('path');
|
||||||
|
const modulePath = dirname(dirname(binPath));
|
||||||
|
uninstallScriptPath = join(modulePath, 'uninstall.sh');
|
||||||
|
|
||||||
|
// Check if the script exists
|
||||||
|
const { access } = await import('fs/promises');
|
||||||
|
await access(uninstallScriptPath);
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't find it in the expected location, try common installation paths
|
||||||
|
const commonPaths = ['/opt/nupst/uninstall.sh', `${process.cwd()}/uninstall.sh`];
|
||||||
|
const { existsSync } = await import('fs');
|
||||||
|
|
||||||
|
uninstallScriptPath = '';
|
||||||
|
for (const path of commonPaths) {
|
||||||
|
if (existsSync(path)) {
|
||||||
|
uninstallScriptPath = path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uninstallScriptPath) {
|
||||||
|
logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
||||||
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close readline before executing script
|
||||||
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
|
|
||||||
|
// Execute uninstall.sh with the appropriate option
|
||||||
|
logger.log('');
|
||||||
|
logger.log(`Running uninstaller from ${uninstallScriptPath}...`);
|
||||||
|
|
||||||
|
// Pass the configuration removal option as an environment variable
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no',
|
||||||
|
REMOVE_REPO: 'yes', // Always remove repo as requested
|
||||||
|
NUPST_CLI_CALL: 'true', // Flag to indicate this is being called from CLI
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the uninstall script with sudo
|
||||||
|
execSync(`sudo bash ${uninstallScriptPath}`, {
|
||||||
|
env,
|
||||||
|
stdio: 'inherit', // Show output in the terminal
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract and remove debug options from args array
|
||||||
|
* @param args Command line arguments
|
||||||
|
* @returns Object with debug flags and cleaned args
|
||||||
|
*/
|
||||||
|
private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } {
|
||||||
|
const debugMode = args.includes('--debug') || args.includes('-d');
|
||||||
|
// Remove debug flags from args
|
||||||
|
const cleanedArgs = args.filter((arg) => arg !== '--debug' && arg !== '-d');
|
||||||
|
|
||||||
|
return { debugMode, cleanedArgs };
|
||||||
|
}
|
||||||
|
}
|
1029
ts/cli/ups-handler.ts
Normal file
1029
ts/cli/ups-handler.ts
Normal file
File diff suppressed because it is too large
Load Diff
88
ts/colors.ts
Normal file
88
ts/colors.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Color theme and styling utilities for NUPST CLI
|
||||||
|
* Uses Deno standard library colors module
|
||||||
|
*/
|
||||||
|
import * as colors from '@std/fmt/colors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color theme for consistent CLI styling
|
||||||
|
*/
|
||||||
|
export const theme = {
|
||||||
|
// Message types
|
||||||
|
error: colors.red,
|
||||||
|
warning: colors.yellow,
|
||||||
|
success: colors.green,
|
||||||
|
info: colors.cyan,
|
||||||
|
dim: colors.dim,
|
||||||
|
highlight: colors.bold,
|
||||||
|
|
||||||
|
// Status indicators
|
||||||
|
statusActive: (text: string) => colors.green(colors.bold(text)),
|
||||||
|
statusInactive: (text: string) => colors.red(text),
|
||||||
|
statusWarning: (text: string) => colors.yellow(text),
|
||||||
|
statusUnknown: (text: string) => colors.dim(text),
|
||||||
|
|
||||||
|
// Battery level colors
|
||||||
|
batteryGood: colors.green, // > 60%
|
||||||
|
batteryMedium: colors.yellow, // 30-60%
|
||||||
|
batteryCritical: colors.red, // < 30%
|
||||||
|
|
||||||
|
// Box borders
|
||||||
|
borderSuccess: colors.green,
|
||||||
|
borderError: colors.red,
|
||||||
|
borderWarning: colors.yellow,
|
||||||
|
borderInfo: colors.cyan,
|
||||||
|
borderDefault: (text: string) => text, // No color
|
||||||
|
|
||||||
|
// Command/code highlighting
|
||||||
|
command: colors.cyan,
|
||||||
|
code: colors.dim,
|
||||||
|
path: colors.blue,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status symbols with colors
|
||||||
|
*/
|
||||||
|
export const symbols = {
|
||||||
|
success: colors.green('✓'),
|
||||||
|
error: colors.red('✗'),
|
||||||
|
warning: colors.yellow('⚠'),
|
||||||
|
info: colors.cyan('ℹ'),
|
||||||
|
running: colors.green('●'),
|
||||||
|
stopped: colors.red('○'),
|
||||||
|
starting: colors.yellow('◐'),
|
||||||
|
unknown: colors.dim('◯'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for battery level
|
||||||
|
*/
|
||||||
|
export function getBatteryColor(percentage: number): (text: string) => string {
|
||||||
|
if (percentage >= 60) return theme.batteryGood;
|
||||||
|
if (percentage >= 30) return theme.batteryMedium;
|
||||||
|
return theme.batteryCritical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for runtime remaining
|
||||||
|
*/
|
||||||
|
export function getRuntimeColor(minutes: number): (text: string) => string {
|
||||||
|
if (minutes >= 20) return theme.batteryGood;
|
||||||
|
if (minutes >= 10) return theme.batteryMedium;
|
||||||
|
return theme.batteryCritical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format UPS power status with color
|
||||||
|
*/
|
||||||
|
export function formatPowerStatus(status: 'online' | 'onBattery' | 'unknown'): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'online':
|
||||||
|
return theme.success('Online');
|
||||||
|
case 'onBattery':
|
||||||
|
return theme.warning('On Battery');
|
||||||
|
case 'unknown':
|
||||||
|
default:
|
||||||
|
return theme.dim('Unknown');
|
||||||
|
}
|
||||||
|
}
|
981
ts/daemon.ts
981
ts/daemon.ts
File diff suppressed because it is too large
Load Diff
1
ts/helpers/index.ts
Normal file
1
ts/helpers/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './shortid.ts';
|
22
ts/helpers/shortid.ts
Normal file
22
ts/helpers/shortid.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Generate a short unique ID of 6 alphanumeric characters
|
||||||
|
* @returns A 6-character alphanumeric string
|
||||||
|
*/
|
||||||
|
export function shortId(): string {
|
||||||
|
// Define the character set: a-z, A-Z, 0-9
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
|
||||||
|
// Generate cryptographically secure random values
|
||||||
|
const randomValues = new Uint8Array(6);
|
||||||
|
crypto.getRandomValues(randomValues);
|
||||||
|
|
||||||
|
// Map each random value to a character in our set
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
// Use modulo to map the random byte to a character index
|
||||||
|
const index = randomValues[i] % chars.length;
|
||||||
|
result += chars[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
@@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { NupstCli } from './cli.js';
|
import { NupstCli } from './cli.ts';
|
||||||
|
import { logger } from './logger.ts';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry point for NUPST
|
* Main entry point for NUPST
|
||||||
@@ -12,7 +14,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the main function and handle any errors
|
// Run the main function and handle any errors
|
||||||
main().catch(error => {
|
main().catch((error) => {
|
||||||
console.error('Error:', error);
|
logger.error(`Error: ${error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
333
ts/logger.ts
Normal file
333
ts/logger.ts
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import { theme, symbols } from './colors.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table column alignment options
|
||||||
|
*/
|
||||||
|
export type TColumnAlign = 'left' | 'right' | 'center';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table column definition
|
||||||
|
*/
|
||||||
|
export interface ITableColumn {
|
||||||
|
/** Column header text */
|
||||||
|
header: string;
|
||||||
|
/** Column key in data object */
|
||||||
|
key: string;
|
||||||
|
/** Column alignment (default: left) */
|
||||||
|
align?: TColumnAlign;
|
||||||
|
/** Column width (auto-calculated if not specified) */
|
||||||
|
width?: number;
|
||||||
|
/** Color function to apply to cell values */
|
||||||
|
color?: (value: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Box style types with colors
|
||||||
|
*/
|
||||||
|
export type TBoxStyle = 'default' | 'success' | 'error' | 'warning' | 'info';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 currentBoxStyle: TBoxStyle = 'default';
|
||||||
|
private static instance: Logger;
|
||||||
|
|
||||||
|
/** Default width to use when no width is specified */
|
||||||
|
private readonly DEFAULT_WIDTH = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (red with ✗ symbol)
|
||||||
|
* @param message Error message to log
|
||||||
|
*/
|
||||||
|
public error(message: string): void {
|
||||||
|
console.error(`${symbols.error} ${theme.error(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a warning message (yellow with ⚠ symbol)
|
||||||
|
* @param message Warning message to log
|
||||||
|
*/
|
||||||
|
public warn(message: string): void {
|
||||||
|
console.warn(`${symbols.warning} ${theme.warning(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a success message (green with ✓ symbol)
|
||||||
|
* @param message Success message to log
|
||||||
|
*/
|
||||||
|
public success(message: string): void {
|
||||||
|
console.log(`${symbols.success} ${theme.success(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an info message (cyan with ℹ symbol)
|
||||||
|
* @param message Info message to log
|
||||||
|
*/
|
||||||
|
public info(message: string): void {
|
||||||
|
console.log(`${symbols.info} ${theme.info(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a dim/secondary message
|
||||||
|
* @param message Message to log in dim style
|
||||||
|
*/
|
||||||
|
public dim(message: string): void {
|
||||||
|
console.log(theme.dim(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a highlighted/bold message
|
||||||
|
* @param message Message to highlight
|
||||||
|
*/
|
||||||
|
public highlight(message: string): void {
|
||||||
|
console.log(theme.highlight(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color function for box based on style
|
||||||
|
*/
|
||||||
|
private getBoxColor(style: TBoxStyle): (text: string) => string {
|
||||||
|
switch (style) {
|
||||||
|
case 'success':
|
||||||
|
return theme.borderSuccess;
|
||||||
|
case 'error':
|
||||||
|
return theme.borderError;
|
||||||
|
case 'warning':
|
||||||
|
return theme.borderWarning;
|
||||||
|
case 'info':
|
||||||
|
return theme.borderInfo;
|
||||||
|
case 'default':
|
||||||
|
default:
|
||||||
|
return theme.borderDefault;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a logbox title and set the current box width
|
||||||
|
* @param title Title of the logbox
|
||||||
|
* @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH
|
||||||
|
* @param style Box style for coloring (default, success, error, warning, info)
|
||||||
|
*/
|
||||||
|
public logBoxTitle(title: string, width?: number, style?: TBoxStyle): void {
|
||||||
|
this.currentBoxWidth = width || this.DEFAULT_WIDTH;
|
||||||
|
this.currentBoxStyle = style || 'default';
|
||||||
|
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
|
// Create the title line with appropriate padding
|
||||||
|
const paddedTitle = ` ${title} `;
|
||||||
|
const remainingSpace = this.currentBoxWidth - 3 - paddedTitle.length;
|
||||||
|
|
||||||
|
// Title line: ┌─ Title ───┐
|
||||||
|
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`;
|
||||||
|
|
||||||
|
console.log(colorFn(titleLine));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a logbox line
|
||||||
|
* @param content Content of the line
|
||||||
|
* @param width Optional width override. If not provided, uses the current box width or DEFAULT_WIDTH.
|
||||||
|
*/
|
||||||
|
public logBoxLine(content: string, width?: number): void {
|
||||||
|
if (!this.currentBoxWidth && !width) {
|
||||||
|
// No current width and no width provided, use default width
|
||||||
|
this.logBoxTitle('', this.DEFAULT_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
|
// Calculate the available space for content (use visible length)
|
||||||
|
const availableSpace = boxWidth - 2; // Account for left and right borders
|
||||||
|
const visibleLen = this.visibleLength(content);
|
||||||
|
|
||||||
|
if (visibleLen <= availableSpace - 1) {
|
||||||
|
// If content fits with at least one space for the right border stripe
|
||||||
|
const padding = availableSpace - visibleLen - 1;
|
||||||
|
const line = `│ ${content}${' '.repeat(padding)}│`;
|
||||||
|
console.log(colorFn(line));
|
||||||
|
} else {
|
||||||
|
// Content is too long, let it flow out of boundaries.
|
||||||
|
const line = `│ ${content}`;
|
||||||
|
console.log(colorFn(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a logbox end
|
||||||
|
* @param width Optional width override. If not provided, uses the current box width or DEFAULT_WIDTH.
|
||||||
|
*/
|
||||||
|
public logBoxEnd(width?: number): void {
|
||||||
|
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
|
// Create the bottom border: └────────┘
|
||||||
|
const bottomLine = `└${'─'.repeat(boxWidth - 2)}┘`;
|
||||||
|
console.log(colorFn(bottomLine));
|
||||||
|
|
||||||
|
// Reset the current box width and style
|
||||||
|
this.currentBoxWidth = null;
|
||||||
|
this.currentBoxStyle = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, defaults to DEFAULT_WIDTH
|
||||||
|
* @param style Box style for coloring
|
||||||
|
*/
|
||||||
|
public logBox(title: string, lines: string[], width?: number, style?: TBoxStyle): void {
|
||||||
|
this.logBoxTitle(title, width || this.DEFAULT_WIDTH, style);
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
this.logBoxLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logBoxEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a divider line
|
||||||
|
* @param width Width of the divider, defaults to DEFAULT_WIDTH
|
||||||
|
* @param character Character to use for the divider (default: ─)
|
||||||
|
*/
|
||||||
|
public logDivider(width?: number, character: string = '─'): void {
|
||||||
|
console.log(character.repeat(width || this.DEFAULT_WIDTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip ANSI color codes from string for accurate length calculation
|
||||||
|
*/
|
||||||
|
private stripAnsi(text: string): string {
|
||||||
|
// Remove ANSI escape codes
|
||||||
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get visible length of string (excluding ANSI codes)
|
||||||
|
*/
|
||||||
|
private visibleLength(text: string): number {
|
||||||
|
return this.stripAnsi(text).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Align text within a column (handles ANSI color codes correctly)
|
||||||
|
*/
|
||||||
|
private alignText(text: string, width: number, align: TColumnAlign = 'left'): string {
|
||||||
|
const visibleLen = this.visibleLength(text);
|
||||||
|
|
||||||
|
if (visibleLen >= width) {
|
||||||
|
// Text is too long, truncate the visible part
|
||||||
|
const stripped = this.stripAnsi(text);
|
||||||
|
return stripped.substring(0, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = width - visibleLen;
|
||||||
|
|
||||||
|
switch (align) {
|
||||||
|
case 'right':
|
||||||
|
return ' '.repeat(padding) + text;
|
||||||
|
case 'center': {
|
||||||
|
const leftPad = Math.floor(padding / 2);
|
||||||
|
const rightPad = padding - leftPad;
|
||||||
|
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
|
||||||
|
}
|
||||||
|
case 'left':
|
||||||
|
default:
|
||||||
|
return text + ' '.repeat(padding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a formatted table
|
||||||
|
* @param columns Column definitions
|
||||||
|
* @param rows Array of data objects
|
||||||
|
* @param title Optional table title
|
||||||
|
*/
|
||||||
|
public logTable(columns: ITableColumn[], rows: Record<string, string>[], title?: string): void {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
this.dim('No data to display');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate column widths
|
||||||
|
const columnWidths = columns.map((col) => {
|
||||||
|
if (col.width) return col.width;
|
||||||
|
|
||||||
|
// Auto-calculate width based on header and data (use visible length)
|
||||||
|
let maxWidth = this.visibleLength(col.header);
|
||||||
|
for (const row of rows) {
|
||||||
|
const value = String(row[col.key] || '');
|
||||||
|
maxWidth = Math.max(maxWidth, this.visibleLength(value));
|
||||||
|
}
|
||||||
|
return maxWidth;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total table width
|
||||||
|
const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0) + (columns.length * 3) + 1;
|
||||||
|
|
||||||
|
// Print title if provided
|
||||||
|
if (title) {
|
||||||
|
this.logBoxTitle(title, totalWidth);
|
||||||
|
} else {
|
||||||
|
// Print top border
|
||||||
|
console.log('┌' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print header row
|
||||||
|
const headerCells = columns.map((col, i) =>
|
||||||
|
theme.highlight(this.alignText(col.header, columnWidths[i], col.align))
|
||||||
|
);
|
||||||
|
console.log('│ ' + headerCells.join(' │ ') + ' │');
|
||||||
|
|
||||||
|
// Print separator
|
||||||
|
console.log('├' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤');
|
||||||
|
|
||||||
|
// Print data rows
|
||||||
|
for (const row of rows) {
|
||||||
|
const cells = columns.map((col, i) => {
|
||||||
|
const value = String(row[col.key] || '');
|
||||||
|
const aligned = this.alignText(value, columnWidths[i], col.align);
|
||||||
|
return col.color ? col.color(aligned) : aligned;
|
||||||
|
});
|
||||||
|
console.log('│ ' + cells.join(' │ ') + ' │');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print bottom border
|
||||||
|
console.log('└' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export a singleton instance for easy use
|
||||||
|
export const logger = Logger.getInstance();
|
54
ts/migrations/base-migration.ts
Normal file
54
ts/migrations/base-migration.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Abstract base class for configuration migrations
|
||||||
|
*
|
||||||
|
* Each migration represents an upgrade from one config version to another.
|
||||||
|
* Migrations run in order based on the `order` field, allowing users to jump
|
||||||
|
* multiple versions (e.g., v1 → v4 runs migrations 2, 3, and 4).
|
||||||
|
*/
|
||||||
|
export abstract class BaseMigration {
|
||||||
|
/**
|
||||||
|
* Migration order number
|
||||||
|
* - Order 2: v1 → v2
|
||||||
|
* - Order 3: v2 → v3
|
||||||
|
* - Order 4: v3 → v4
|
||||||
|
* etc.
|
||||||
|
*/
|
||||||
|
abstract readonly order: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source version this migration upgrades from
|
||||||
|
* e.g., "1.x", "3.x"
|
||||||
|
*/
|
||||||
|
abstract readonly fromVersion: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target version this migration upgrades to
|
||||||
|
* e.g., "2.0", "4.0"
|
||||||
|
*/
|
||||||
|
abstract readonly toVersion: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this migration should run on the given config
|
||||||
|
*
|
||||||
|
* @param config - Raw configuration object to check
|
||||||
|
* @returns True if migration should run, false otherwise
|
||||||
|
*/
|
||||||
|
abstract shouldRun(config: any): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the migration on the given config
|
||||||
|
*
|
||||||
|
* @param config - Raw configuration object to migrate
|
||||||
|
* @returns Migrated configuration object
|
||||||
|
*/
|
||||||
|
abstract migrate(config: any): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable name for this migration
|
||||||
|
*
|
||||||
|
* @returns Migration name
|
||||||
|
*/
|
||||||
|
getName(): string {
|
||||||
|
return `Migration ${this.fromVersion} → ${this.toVersion}`;
|
||||||
|
}
|
||||||
|
}
|
10
ts/migrations/index.ts
Normal file
10
ts/migrations/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Configuration migrations module
|
||||||
|
*
|
||||||
|
* Exports the migration system for upgrading configs between versions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { BaseMigration } from './base-migration.ts';
|
||||||
|
export { MigrationRunner } from './migration-runner.ts';
|
||||||
|
export { MigrationV1ToV2 } from './migration-v1-to-v2.ts';
|
||||||
|
export { MigrationV3ToV4 } from './migration-v3-to-v4.ts';
|
71
ts/migrations/migration-runner.ts
Normal file
71
ts/migrations/migration-runner.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { BaseMigration } from './base-migration.ts';
|
||||||
|
import { MigrationV1ToV2 } from './migration-v1-to-v2.ts';
|
||||||
|
import { MigrationV3ToV4 } from './migration-v3-to-v4.ts';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration runner
|
||||||
|
*
|
||||||
|
* Discovers all available migrations, sorts them by order,
|
||||||
|
* and runs applicable migrations in sequence.
|
||||||
|
*/
|
||||||
|
export class MigrationRunner {
|
||||||
|
private migrations: BaseMigration[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Register all migrations here
|
||||||
|
this.migrations = [
|
||||||
|
new MigrationV1ToV2(),
|
||||||
|
new MigrationV3ToV4(),
|
||||||
|
// Add future migrations here (v4→v5, v5→v6, etc.)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sort by order to ensure they run in sequence
|
||||||
|
this.migrations.sort((a, b) => a.order - b.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run all applicable migrations on the config
|
||||||
|
*
|
||||||
|
* @param config - Raw configuration object to migrate
|
||||||
|
* @returns Migrated configuration and whether migrations ran
|
||||||
|
*/
|
||||||
|
async run(config: any): Promise<{ config: any; migrated: boolean }> {
|
||||||
|
let currentConfig = config;
|
||||||
|
let anyMigrationsRan = false;
|
||||||
|
|
||||||
|
for (const migration of this.migrations) {
|
||||||
|
const shouldRun = await migration.shouldRun(currentConfig);
|
||||||
|
|
||||||
|
if (shouldRun) {
|
||||||
|
// Only show "checking" message when we actually need to migrate
|
||||||
|
if (!anyMigrationsRan) {
|
||||||
|
logger.dim('Checking for required config migrations...');
|
||||||
|
}
|
||||||
|
logger.info(`Running ${migration.getName()}...`);
|
||||||
|
currentConfig = await migration.migrate(currentConfig);
|
||||||
|
anyMigrationsRan = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyMigrationsRan) {
|
||||||
|
logger.success('Configuration migrations complete');
|
||||||
|
} else {
|
||||||
|
logger.success('config format ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: currentConfig,
|
||||||
|
migrated: anyMigrationsRan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered migrations
|
||||||
|
*
|
||||||
|
* @returns Array of all migrations sorted by order
|
||||||
|
*/
|
||||||
|
getMigrations(): BaseMigration[] {
|
||||||
|
return [...this.migrations];
|
||||||
|
}
|
||||||
|
}
|
56
ts/migrations/migration-v1-to-v2.ts
Normal file
56
ts/migrations/migration-v1-to-v2.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { BaseMigration } from './base-migration.ts';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from v1 (single SNMP config) to v2 (upsDevices array)
|
||||||
|
*
|
||||||
|
* Detects old format:
|
||||||
|
* {
|
||||||
|
* snmp: { ... },
|
||||||
|
* thresholds: { ... },
|
||||||
|
* checkInterval: 30000
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Converts to:
|
||||||
|
* {
|
||||||
|
* version: "2.0",
|
||||||
|
* upsDevices: [{ id: "default", name: "Default UPS", snmp: ..., thresholds: ... }],
|
||||||
|
* groups: [],
|
||||||
|
* checkInterval: 30000
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export class MigrationV1ToV2 extends BaseMigration {
|
||||||
|
readonly order = 2;
|
||||||
|
readonly fromVersion = '1.x';
|
||||||
|
readonly toVersion = '2.0';
|
||||||
|
|
||||||
|
async shouldRun(config: any): Promise<boolean> {
|
||||||
|
// V1 format has snmp field directly at root, no upsDevices or upsList
|
||||||
|
return !!config.snmp && !config.upsDevices && !config.upsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate(config: any): Promise<any> {
|
||||||
|
logger.info(`${this.getName()}: Converting single SNMP config to multi-UPS format...`);
|
||||||
|
|
||||||
|
const migrated = {
|
||||||
|
version: this.toVersion,
|
||||||
|
upsDevices: [
|
||||||
|
{
|
||||||
|
id: 'default',
|
||||||
|
name: 'Default UPS',
|
||||||
|
snmp: config.snmp,
|
||||||
|
thresholds: config.thresholds || {
|
||||||
|
battery: 60,
|
||||||
|
runtime: 20,
|
||||||
|
},
|
||||||
|
groups: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
groups: [],
|
||||||
|
checkInterval: config.checkInterval || 30000,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.success(`${this.getName()}: Migration complete`);
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
}
|
119
ts/migrations/migration-v3-to-v4.ts
Normal file
119
ts/migrations/migration-v3-to-v4.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { BaseMigration } from './base-migration.ts';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from v3 (upsList) to v4 (upsDevices)
|
||||||
|
*
|
||||||
|
* Transforms v3 format with flat SNMP config:
|
||||||
|
* {
|
||||||
|
* upsList: [
|
||||||
|
* {
|
||||||
|
* id: "ups-1",
|
||||||
|
* name: "UPS 1",
|
||||||
|
* host: "192.168.1.1",
|
||||||
|
* port: 161,
|
||||||
|
* community: "public",
|
||||||
|
* version: "1" // string
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* To v4 format with nested SNMP config:
|
||||||
|
* {
|
||||||
|
* version: "4.0",
|
||||||
|
* upsDevices: [
|
||||||
|
* {
|
||||||
|
* id: "ups-1",
|
||||||
|
* name: "UPS 1",
|
||||||
|
* snmp: {
|
||||||
|
* host: "192.168.1.1",
|
||||||
|
* port: 161,
|
||||||
|
* community: "public",
|
||||||
|
* version: 1, // number
|
||||||
|
* timeout: 5000
|
||||||
|
* },
|
||||||
|
* thresholds: { battery: 60, runtime: 20 },
|
||||||
|
* groups: []
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export class MigrationV3ToV4 extends BaseMigration {
|
||||||
|
readonly order = 4;
|
||||||
|
readonly fromVersion = '3.x';
|
||||||
|
readonly toVersion = '4.0';
|
||||||
|
|
||||||
|
async shouldRun(config: any): Promise<boolean> {
|
||||||
|
// V3 format has upsList OR has upsDevices with flat structure (host at top level)
|
||||||
|
if (config.upsList && !config.upsDevices) {
|
||||||
|
return true; // Classic v3 with upsList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upsDevices exists but has flat structure (v3 format)
|
||||||
|
if (config.upsDevices && config.upsDevices.length > 0) {
|
||||||
|
const firstDevice = config.upsDevices[0];
|
||||||
|
// V3 has host at top level, v4 has it nested in snmp object
|
||||||
|
return !!firstDevice.host && !firstDevice.snmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate(config: any): Promise<any> {
|
||||||
|
logger.info(`${this.getName()}: Migrating v3 config to v4 format...`);
|
||||||
|
logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`);
|
||||||
|
|
||||||
|
// Get devices from either upsList or upsDevices (for partially migrated configs)
|
||||||
|
const sourceDevices = config.upsList || config.upsDevices;
|
||||||
|
|
||||||
|
// Transform each UPS device from v3 flat structure to v4 nested structure
|
||||||
|
const transformedDevices = sourceDevices.map((device: any) => {
|
||||||
|
// Build SNMP config object
|
||||||
|
const snmpConfig: any = {
|
||||||
|
host: device.host,
|
||||||
|
port: device.port || 161,
|
||||||
|
version: typeof device.version === 'string' ? parseInt(device.version, 10) : device.version,
|
||||||
|
timeout: device.timeout || 5000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add SNMPv1/v2c fields
|
||||||
|
if (device.community) {
|
||||||
|
snmpConfig.community = device.community;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add SNMPv3 fields
|
||||||
|
if (device.securityLevel) snmpConfig.securityLevel = device.securityLevel;
|
||||||
|
if (device.username) snmpConfig.username = device.username;
|
||||||
|
if (device.authProtocol) snmpConfig.authProtocol = device.authProtocol;
|
||||||
|
if (device.authKey) snmpConfig.authKey = device.authKey;
|
||||||
|
if (device.privProtocol) snmpConfig.privProtocol = device.privProtocol;
|
||||||
|
if (device.privKey) snmpConfig.privKey = device.privKey;
|
||||||
|
|
||||||
|
// Add UPS model if present
|
||||||
|
if (device.upsModel) snmpConfig.upsModel = device.upsModel;
|
||||||
|
if (device.customOIDs) snmpConfig.customOIDs = device.customOIDs;
|
||||||
|
|
||||||
|
// Return v4 format with nested structure
|
||||||
|
return {
|
||||||
|
id: device.id,
|
||||||
|
name: device.name,
|
||||||
|
snmp: snmpConfig,
|
||||||
|
thresholds: device.thresholds || {
|
||||||
|
battery: 60,
|
||||||
|
runtime: 20,
|
||||||
|
},
|
||||||
|
groups: device.groups || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const migrated = {
|
||||||
|
version: this.toVersion,
|
||||||
|
upsDevices: transformedDevices,
|
||||||
|
groups: config.groups || [],
|
||||||
|
checkInterval: config.checkInterval || 30000,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.success(`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`);
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
}
|
192
ts/nupst.ts
192
ts/nupst.ts
@@ -1,6 +1,12 @@
|
|||||||
import { NupstSnmp } from './snmp.js';
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import { NupstDaemon } from './daemon.js';
|
import { NupstDaemon } from './daemon.ts';
|
||||||
import { NupstSystemd } from './systemd.js';
|
import { NupstSystemd } from './systemd.ts';
|
||||||
|
import { commitinfo } from './00_commitinfo_data.ts';
|
||||||
|
import { logger } from './logger.ts';
|
||||||
|
import { UpsHandler } from './cli/ups-handler.ts';
|
||||||
|
import { GroupHandler } from './cli/group-handler.ts';
|
||||||
|
import { ServiceHandler } from './cli/service-handler.ts';
|
||||||
|
import * as https from 'node:https';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Nupst class that coordinates all components
|
* Main Nupst class that coordinates all components
|
||||||
@@ -10,14 +16,26 @@ export class Nupst {
|
|||||||
private readonly snmp: NupstSnmp;
|
private readonly snmp: NupstSnmp;
|
||||||
private readonly daemon: NupstDaemon;
|
private readonly daemon: NupstDaemon;
|
||||||
private readonly systemd: NupstSystemd;
|
private readonly systemd: NupstSystemd;
|
||||||
|
private readonly upsHandler: UpsHandler;
|
||||||
|
private readonly groupHandler: GroupHandler;
|
||||||
|
private readonly serviceHandler: ServiceHandler;
|
||||||
|
private updateAvailable: boolean = false;
|
||||||
|
private latestVersion: string = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Nupst instance with all necessary components
|
* Create a new Nupst instance with all necessary components
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// Initialize core components
|
||||||
this.snmp = new NupstSnmp();
|
this.snmp = new NupstSnmp();
|
||||||
|
this.snmp.setNupst(this); // Set up bidirectional reference
|
||||||
this.daemon = new NupstDaemon(this.snmp);
|
this.daemon = new NupstDaemon(this.snmp);
|
||||||
this.systemd = new NupstSystemd(this.daemon);
|
this.systemd = new NupstSystemd(this.daemon);
|
||||||
|
|
||||||
|
// Initialize handlers
|
||||||
|
this.upsHandler = new UpsHandler(this);
|
||||||
|
this.groupHandler = new GroupHandler(this);
|
||||||
|
this.serviceHandler = new ServiceHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,4 +58,172 @@ export class Nupst {
|
|||||||
public getSystemd(): NupstSystemd {
|
public getSystemd(): NupstSystemd {
|
||||||
return this.systemd;
|
return this.systemd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the UPS handler for UPS management
|
||||||
|
*/
|
||||||
|
public getUpsHandler(): UpsHandler {
|
||||||
|
return this.upsHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Group handler for group management
|
||||||
|
*/
|
||||||
|
public getGroupHandler(): GroupHandler {
|
||||||
|
return this.groupHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Service handler for service management
|
||||||
|
*/
|
||||||
|
public getServiceHandler(): ServiceHandler {
|
||||||
|
return this.serviceHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current version of NUPST
|
||||||
|
* @returns The current version string
|
||||||
|
*/
|
||||||
|
public getVersion(): string {
|
||||||
|
return commitinfo.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an update is available
|
||||||
|
* @returns Promise resolving to true if an update is available
|
||||||
|
*/
|
||||||
|
public async checkForUpdates(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const latestVersion = await this.getLatestVersion();
|
||||||
|
const currentVersion = this.getVersion();
|
||||||
|
|
||||||
|
// Compare versions
|
||||||
|
this.updateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
|
||||||
|
this.latestVersion = latestVersion;
|
||||||
|
|
||||||
|
return this.updateAvailable;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error checking for updates: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update status information
|
||||||
|
* @returns Object with update status information
|
||||||
|
*/
|
||||||
|
public getUpdateStatus(): {
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string;
|
||||||
|
updateAvailable: boolean;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
currentVersion: this.getVersion(),
|
||||||
|
latestVersion: this.latestVersion || this.getVersion(),
|
||||||
|
updateAvailable: this.updateAvailable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest version from npm registry
|
||||||
|
* @returns Promise resolving to the latest version string
|
||||||
|
*/
|
||||||
|
private getLatestVersion(): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: 'registry.npmjs.org',
|
||||||
|
path: '/@serve.zone/nupst',
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': `nupst/${this.getVersion()}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(data);
|
||||||
|
if (response['dist-tags'] && response['dist-tags'].latest) {
|
||||||
|
resolve(response['dist-tags'].latest);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Failed to parse version from npm registry response'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two semantic version strings
|
||||||
|
* @param versionA First version
|
||||||
|
* @param versionB Second version
|
||||||
|
* @returns -1 if versionA < versionB, 0 if equal, 1 if versionA > versionB
|
||||||
|
*/
|
||||||
|
private compareVersions(versionA: string, versionB: string): number {
|
||||||
|
const partsA = versionA.split('.').map((part) => parseInt(part, 10));
|
||||||
|
const partsB = versionB.split('.').map((part) => parseInt(part, 10));
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||||
|
const partA = i < partsA.length ? partsA[i] : 0;
|
||||||
|
const partB = i < partsB.length ? partsB[i] : 0;
|
||||||
|
|
||||||
|
if (partA > partB) return 1;
|
||||||
|
if (partA < partB) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Versions are equal
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the current version and update status
|
||||||
|
*/
|
||||||
|
public logVersionInfo(checkForUpdates: boolean = true): void {
|
||||||
|
const version = this.getVersion();
|
||||||
|
const boxWidth = 45;
|
||||||
|
|
||||||
|
logger.logBoxTitle('NUPST Version', boxWidth);
|
||||||
|
logger.logBoxLine(`Current Version: ${version}`);
|
||||||
|
|
||||||
|
if (this.updateAvailable && this.latestVersion) {
|
||||||
|
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||||
|
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
} else if (checkForUpdates) {
|
||||||
|
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) {
|
||||||
|
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||||
|
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||||
|
} else {
|
||||||
|
logger.logBoxLine('You are running the latest version');
|
||||||
|
}
|
||||||
|
logger.logBoxEnd();
|
||||||
|
}).catch(() => {
|
||||||
|
logger.logBoxLine('Could not check for updates');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Re-export all public types
|
// Re-export all public types
|
||||||
export type { UpsStatus, OIDSet, UpsModel, SnmpConfig } from './types.js';
|
export type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
||||||
|
|
||||||
// Re-export the SNMP manager class
|
// Re-export the SNMP manager class
|
||||||
export { NupstSnmp } from './manager.js';
|
export { NupstSnmp } from './manager.ts';
|
||||||
|
@@ -1,12 +1,7 @@
|
|||||||
import { exec } from 'child_process';
|
import * as snmp from 'npm:net-snmp@3.20.0';
|
||||||
import { promisify } from 'util';
|
import { Buffer } from 'node:buffer';
|
||||||
import * as dgram from 'dgram';
|
import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
||||||
import type { OIDSet, SnmpConfig, UpsModel, UpsStatus } from './types.js';
|
import { UpsOidSets } from './oid-sets.ts';
|
||||||
import { UpsOidSets } from './oid-sets.js';
|
|
||||||
import { SnmpPacketCreator } from './packet-creator.js';
|
|
||||||
import { SnmpPacketParser } from './packet-parser.js';
|
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for SNMP communication with UPS devices
|
* Class for SNMP communication with UPS devices
|
||||||
@@ -14,10 +9,14 @@ const execAsync = promisify(exec);
|
|||||||
*/
|
*/
|
||||||
export class NupstSnmp {
|
export class NupstSnmp {
|
||||||
// Active OID set
|
// Active OID set
|
||||||
private activeOIDs: OIDSet;
|
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
|
// Default SNMP configuration
|
||||||
private readonly DEFAULT_CONFIG: SnmpConfig = {
|
private readonly DEFAULT_CONFIG: ISnmpConfig = {
|
||||||
host: '127.0.0.1', // Default to localhost
|
host: '127.0.0.1', // Default to localhost
|
||||||
port: 161, // Default SNMP port
|
port: 161, // Default SNMP port
|
||||||
community: 'public', // Default community string for v1/v2c
|
community: 'public', // Default community string for v1/v2c
|
||||||
@@ -26,13 +25,6 @@ export class NupstSnmp {
|
|||||||
upsModel: 'cyberpower', // Default UPS model
|
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
|
* Create a new SNMP manager
|
||||||
* @param debug Whether to enable debug mode
|
* @param debug Whether to enable debug mode
|
||||||
@@ -42,30 +34,22 @@ export class NupstSnmp {
|
|||||||
// Set default OID set
|
// Set default OID set
|
||||||
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
|
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set active OID set based on UPS model
|
* Set reference to the main Nupst instance
|
||||||
* @param config SNMP configuration
|
* @param nupst Reference to the main Nupst instance
|
||||||
*/
|
*/
|
||||||
private setActiveOIDs(config: SnmpConfig): void {
|
public setNupst(nupst: any): void {
|
||||||
// If custom OIDs are provided, use them
|
this.nupst = nupst;
|
||||||
if (config.upsModel === 'custom' && config.customOIDs) {
|
|
||||||
this.activeOIDs = config.customOIDs;
|
|
||||||
if (this.debug) {
|
|
||||||
console.log('Using custom OIDs:', this.activeOIDs);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use OIDs for the specified UPS model or default to Cyberpower
|
|
||||||
const model = config.upsModel || 'cyberpower';
|
|
||||||
this.activeOIDs = UpsOidSets.getOidSet(model);
|
|
||||||
|
|
||||||
if (this.debug) {
|
|
||||||
console.log(`Using OIDs for UPS model: ${model}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get reference to the main Nupst instance
|
||||||
|
*/
|
||||||
|
public getNupst(): any {
|
||||||
|
return this.nupst;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable debug mode
|
* Enable debug mode
|
||||||
*/
|
*/
|
||||||
@@ -75,111 +59,217 @@ export class NupstSnmp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an SNMP GET request
|
* Set active OID set based on UPS model
|
||||||
|
* @param config SNMP configuration
|
||||||
|
*/
|
||||||
|
private setActiveOIDs(config: ISnmpConfig): void {
|
||||||
|
// If custom OIDs are provided, use them
|
||||||
|
if (config.upsModel === 'custom' && config.customOIDs) {
|
||||||
|
this.activeOIDs = config.customOIDs;
|
||||||
|
if (this.debug) {
|
||||||
|
console.log('Using custom OIDs:', this.activeOIDs);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use OIDs for the specified UPS model or default to Cyberpower
|
||||||
|
const model = config.upsModel || 'cyberpower';
|
||||||
|
this.activeOIDs = UpsOidSets.getOidSet(model);
|
||||||
|
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(`Using OIDs for UPS model: ${model}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an SNMP GET request using the net-snmp package
|
||||||
* @param oid OID to query
|
* @param oid OID to query
|
||||||
* @param config SNMP configuration
|
* @param config SNMP configuration
|
||||||
|
* @param retryCount Current retry count (unused in this implementation)
|
||||||
* @returns Promise resolving to the SNMP response value
|
* @returns Promise resolving to the SNMP response value
|
||||||
*/
|
*/
|
||||||
public async snmpGet(oid: string, config = this.DEFAULT_CONFIG): Promise<any> {
|
public snmpGet(
|
||||||
|
oid: string,
|
||||||
|
config = this.DEFAULT_CONFIG,
|
||||||
|
retryCount = 0,
|
||||||
|
): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
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) {
|
if (this.debug) {
|
||||||
console.log(`Sending SNMP ${config.version === 3 ? 'v3' : ('v' + config.version)} request to ${config.host}:${config.port}`);
|
console.log(
|
||||||
console.log('Request length:', request.length);
|
`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`,
|
||||||
console.log('First 16 bytes of request:', request.slice(0, 16).toString('hex'));
|
);
|
||||||
console.log('Full request hex:', request.toString('hex'));
|
console.log('Using community:', config.community);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set timeout - add extra logging for debugging
|
// Create SNMP options based on configuration
|
||||||
const timeout = setTimeout(() => {
|
const options: any = {
|
||||||
socket.close();
|
port: config.port,
|
||||||
if (this.debug) {
|
retries: 2, // Number of retries
|
||||||
console.error('---------------------------------------');
|
timeout: config.timeout,
|
||||||
console.error('SNMP request timed out after', config.timeout, 'ms');
|
transport: 'udp4',
|
||||||
console.error('SNMP Version:', config.version);
|
idBitsSize: 32,
|
||||||
if (config.version === 3) {
|
context: config.context || '',
|
||||||
console.error('SNMPv3 Security Level:', config.securityLevel);
|
};
|
||||||
console.error('SNMPv3 Username:', config.username);
|
|
||||||
console.error('SNMPv3 Auth Protocol:', config.authProtocol || 'None');
|
// Set version based on config
|
||||||
console.error('SNMPv3 Privacy Protocol:', config.privProtocol || 'None');
|
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;
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.error('OID:', oid);
|
} else if (securityLevel === 'authPriv') {
|
||||||
console.error('Host:', config.host);
|
user.level = snmp.SecurityLevel.authPriv;
|
||||||
console.error('Port:', config.port);
|
|
||||||
console.error('---------------------------------------');
|
// Set auth protocol - must provide both protocol and key
|
||||||
}
|
if (config.authProtocol && config.authKey) {
|
||||||
reject(new Error(`SNMP request timed out after ${config.timeout}ms`));
|
if (config.authProtocol === 'MD5') {
|
||||||
}, config.timeout);
|
user.authProtocol = snmp.AuthProtocols.md5;
|
||||||
|
} else if (config.authProtocol === 'SHA') {
|
||||||
// Listen for responses
|
user.authProtocol = snmp.AuthProtocols.sha;
|
||||||
socket.on('message', (message, rinfo) => {
|
}
|
||||||
clearTimeout(timeout);
|
user.authKey = config.authKey;
|
||||||
|
|
||||||
if (this.debug) {
|
// Set privacy protocol - must provide both protocol and key
|
||||||
console.log(`Received SNMP response from ${rinfo.address}:${rinfo.port}`);
|
if (config.privProtocol && config.privKey) {
|
||||||
console.log('Response length:', message.length);
|
if (config.privProtocol === 'DES') {
|
||||||
console.log('First 16 bytes of response:', message.slice(0, 16).toString('hex'));
|
user.privProtocol = snmp.PrivProtocols.des;
|
||||||
console.log('Full response hex:', message.toString('hex'));
|
} else if (config.privProtocol === 'AES') {
|
||||||
}
|
user.privProtocol = snmp.PrivProtocols.aes;
|
||||||
|
}
|
||||||
try {
|
user.privKey = config.privKey;
|
||||||
const result = SnmpPacketParser.parseSnmpResponse(message, config, this.debug);
|
} else {
|
||||||
|
// Fallback to authNoPriv if priv details missing
|
||||||
if (this.debug) {
|
user.level = snmp.SecurityLevel.authNoPriv;
|
||||||
console.log('Parsed SNMP response:', result);
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (this.debug) {
|
||||||
console.error('Socket error during SNMP request:', error);
|
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',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
reject(error);
|
|
||||||
});
|
session = snmp.createV3Session(config.host, user, options);
|
||||||
|
} else {
|
||||||
// First send the request directly without binding to a specific port
|
// For SNMPv1/v2c, we use the community string
|
||||||
// This lets the OS pick an available port instead of trying to bind to one
|
session = snmp.createSession(config.host, config.community || 'public', options);
|
||||||
socket.send(request, 0, request.length, config.port, config.host, (error) => {
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (error) {
|
||||||
clearTimeout(timeout);
|
|
||||||
socket.close();
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.error('Error sending SNMP request:', error);
|
console.error('SNMP GET error:', error);
|
||||||
}
|
}
|
||||||
reject(error);
|
reject(new Error(`SNMP GET error: ${error.message || error}`));
|
||||||
} else if (this.debug) {
|
return;
|
||||||
console.log('SNMP request sent successfully');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: number) => 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -189,11 +279,11 @@ export class NupstSnmp {
|
|||||||
* @param config SNMP configuration
|
* @param config SNMP configuration
|
||||||
* @returns Promise resolving to the UPS status
|
* @returns Promise resolving to the UPS status
|
||||||
*/
|
*/
|
||||||
public async getUpsStatus(config = this.DEFAULT_CONFIG): Promise<UpsStatus> {
|
public async getUpsStatus(config = this.DEFAULT_CONFIG): Promise<IUpsStatus> {
|
||||||
try {
|
try {
|
||||||
// Set active OID set based on UPS model in config
|
// Set active OID set based on UPS model in config
|
||||||
this.setActiveOIDs(config);
|
this.setActiveOIDs(config);
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('---------------------------------------');
|
console.log('---------------------------------------');
|
||||||
console.log('Getting UPS status with config:');
|
console.log('Getting UPS status with config:');
|
||||||
@@ -216,144 +306,30 @@ export class NupstSnmp {
|
|||||||
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
||||||
console.log('---------------------------------------');
|
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
|
// Get all values with independent retry logic
|
||||||
const powerStatusValue = await getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status');
|
const powerStatusValue = await this.getSNMPValueWithRetry(
|
||||||
const batteryCapacity = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity') || 0;
|
this.activeOIDs.POWER_STATUS,
|
||||||
const batteryRuntime = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime') || 0;
|
'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
|
// 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
|
// Convert to minutes for UPS models with different time units
|
||||||
if (config.upsModel === 'cyberpower') {
|
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
|
||||||
// CyberPower RMCARD205: upsBaseOutputStatus values
|
|
||||||
// 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc.
|
|
||||||
if (powerStatusValue === 2) {
|
|
||||||
powerStatus = 'online';
|
|
||||||
} else if (powerStatusValue === 3) {
|
|
||||||
powerStatus = 'onBattery';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Default interpretation for other UPS models
|
|
||||||
if (powerStatusValue === 1) {
|
|
||||||
powerStatus = 'online';
|
|
||||||
} else if (powerStatusValue === 2) {
|
|
||||||
powerStatus = 'onBattery';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert TimeTicks to minutes for CyberPower runtime (value is in 1/100 seconds)
|
|
||||||
let processedRuntime = batteryRuntime;
|
|
||||||
if (config.upsModel === 'cyberpower' && batteryRuntime > 0) {
|
|
||||||
// TimeTicks is in 1/100 seconds, convert to minutes
|
|
||||||
processedRuntime = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
|
|
||||||
if (this.debug) {
|
|
||||||
console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${processedRuntime} minutes`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
powerStatus,
|
powerStatus,
|
||||||
batteryCapacity,
|
batteryCapacity,
|
||||||
@@ -364,7 +340,7 @@ export class NupstSnmp {
|
|||||||
batteryRuntime,
|
batteryRuntime,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('---------------------------------------');
|
console.log('---------------------------------------');
|
||||||
console.log('UPS status result:');
|
console.log('UPS status result:');
|
||||||
@@ -373,142 +349,257 @@ export class NupstSnmp {
|
|||||||
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
||||||
console.log('---------------------------------------');
|
console.log('---------------------------------------');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.error('---------------------------------------');
|
console.error('---------------------------------------');
|
||||||
console.error('Error getting UPS status:', error.message);
|
console.error(
|
||||||
|
'Error getting UPS status:',
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
console.error('---------------------------------------');
|
console.error('---------------------------------------');
|
||||||
}
|
}
|
||||||
throw new Error(`Failed to get UPS status: ${error.message}`);
|
throw new Error(
|
||||||
|
`Failed to get UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discover SNMP engine ID (for SNMPv3)
|
* Helper method to get SNMP value with retry and fallback logic
|
||||||
* Sends a proper discovery message to get the engine ID from the device
|
* @param oid OID to query
|
||||||
|
* @param description Description of the value for logging
|
||||||
* @param config SNMP configuration
|
* @param config SNMP configuration
|
||||||
* @returns Promise resolving to the discovered engine ID
|
* @returns Promise resolving to the SNMP value
|
||||||
*/
|
*/
|
||||||
public async discoverEngineId(config: SnmpConfig): Promise<Buffer> {
|
private async getSNMPValueWithRetry(
|
||||||
return new Promise((resolve, reject) => {
|
oid: string,
|
||||||
const socket = dgram.createSocket('udp4');
|
description: string,
|
||||||
|
config: ISnmpConfig,
|
||||||
// Create a proper discovery message (SNMPv3 with noAuthNoPriv)
|
): Promise<any> {
|
||||||
const discoveryConfig: SnmpConfig = {
|
if (oid === '') {
|
||||||
...config,
|
|
||||||
securityLevel: 'noAuthNoPriv',
|
|
||||||
username: '', // Empty username for discovery
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a simple GetRequest for sysDescr (a commonly available OID)
|
|
||||||
const request = SnmpPacketCreator.createDiscoveryMessage(discoveryConfig, this.requestID++);
|
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('Sending SNMPv3 discovery message');
|
console.log(`No OID provided for ${description}, skipping`);
|
||||||
console.log('SNMPv3 Discovery message:', request.toString('hex'));
|
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
// 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(() => {
|
if (this.debug) {
|
||||||
socket.close();
|
console.log(`Getting ${description} OID: ${oid}`);
|
||||||
// Fall back to default engine ID if discovery fails
|
}
|
||||||
if (this.debug) {
|
|
||||||
console.error('---------------------------------------');
|
try {
|
||||||
console.error('Engine ID discovery timed out after', discoveryTimeout, 'ms');
|
const value = await this.snmpGet(oid, config);
|
||||||
console.error('SNMPv3 settings:');
|
if (this.debug) {
|
||||||
console.error(' Username:', config.username);
|
console.log(`${description} value:`, value);
|
||||||
console.error(' Security Level:', config.securityLevel);
|
}
|
||||||
console.error(' Host:', config.host);
|
return value;
|
||||||
console.error(' Port:', config.port);
|
} catch (error) {
|
||||||
console.error('Using default engine ID:', this.engineID.toString('hex'));
|
if (this.debug) {
|
||||||
console.error('---------------------------------------');
|
console.error(
|
||||||
}
|
`Error getting ${description}:`,
|
||||||
resolve(this.engineID);
|
error instanceof Error ? error.message : String(error),
|
||||||
}, discoveryTimeout);
|
);
|
||||||
|
}
|
||||||
// Listen for responses
|
|
||||||
socket.on('message', (message, rinfo) => {
|
// If we're using SNMPv3, try with different security levels
|
||||||
clearTimeout(timeout);
|
if (config.version === 3) {
|
||||||
|
return await this.tryFallbackSecurityLevels(oid, description, config);
|
||||||
if (this.debug) {
|
}
|
||||||
console.log(`Received SNMPv3 discovery response from ${rinfo.address}:${rinfo.port}`);
|
|
||||||
console.log('Response:', message.toString('hex'));
|
// Try with standard OIDs as fallback
|
||||||
}
|
if (config.upsModel !== 'custom') {
|
||||||
|
return await this.tryStandardOids(oid, description, config);
|
||||||
try {
|
}
|
||||||
// Extract engine ID from response
|
|
||||||
const engineId = SnmpPacketParser.extractEngineId(message, this.debug);
|
// Return a default value if all attempts fail
|
||||||
if (engineId) {
|
if (this.debug) {
|
||||||
this.engineID = engineId; // Update the engine ID
|
console.log(`Using default value 0 for ${description}`);
|
||||||
if (this.debug) {
|
}
|
||||||
console.log('Discovered engine ID:', engineId.toString('hex'));
|
return 0;
|
||||||
}
|
}
|
||||||
socket.close();
|
|
||||||
resolve(engineId);
|
|
||||||
} else {
|
|
||||||
if (this.debug) {
|
|
||||||
console.log('Could not extract engine ID, using default');
|
|
||||||
}
|
|
||||||
socket.close();
|
|
||||||
resolve(this.engineID);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (this.debug) {
|
|
||||||
console.error('Error extracting engine ID:', error);
|
|
||||||
}
|
|
||||||
socket.close();
|
|
||||||
resolve(this.engineID); // Fall back to default engine ID
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle errors
|
|
||||||
socket.on('error', (error) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
socket.close();
|
|
||||||
if (this.debug) {
|
|
||||||
console.error('Engine ID discovery socket error:', error);
|
|
||||||
}
|
|
||||||
resolve(this.engineID); // Fall back to default engine ID
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send request directly without binding
|
|
||||||
socket.send(request, 0, request.length, config.port, config.host, (error) => {
|
|
||||||
if (error) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
socket.close();
|
|
||||||
if (this.debug) {
|
|
||||||
console.error('Error sending discovery message:', error);
|
|
||||||
}
|
|
||||||
resolve(this.engineID); // Fall back to default engine ID
|
|
||||||
} else if (this.debug) {
|
|
||||||
console.log('Discovery message sent successfully');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate system shutdown
|
* Try fallback security levels for SNMPv3
|
||||||
* @param reason Reason for shutdown
|
* @param oid OID to query
|
||||||
|
* @param description Description of the value for logging
|
||||||
|
* @param config SNMP configuration
|
||||||
|
* @returns Promise resolving to the SNMP value
|
||||||
*/
|
*/
|
||||||
public async initiateShutdown(reason: string): Promise<void> {
|
private async tryFallbackSecurityLevels(
|
||||||
console.log(`Initiating system shutdown due to: ${reason}`);
|
oid: string,
|
||||||
try {
|
description: string,
|
||||||
// Execute shutdown command
|
config: ISnmpConfig,
|
||||||
const { stdout } = await execAsync('shutdown -h +1 "UPS battery critical, shutting down in 1 minute"');
|
): Promise<any> {
|
||||||
console.log('Shutdown initiated:', stdout);
|
if (this.debug) {
|
||||||
} catch (error) {
|
console.log(`Retrying ${description} with fallback security level...`);
|
||||||
console.error('Failed to initiate shutdown:', error);
|
}
|
||||||
// Try a different method if first one fails
|
|
||||||
|
// Try with authNoPriv if current level is authPriv
|
||||||
|
if (config.securityLevel === 'authPriv') {
|
||||||
|
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
|
||||||
try {
|
try {
|
||||||
console.log('Trying alternative shutdown method...');
|
if (this.debug) {
|
||||||
await execAsync('poweroff --force');
|
console.log(`Retrying with authNoPriv security level`);
|
||||||
} catch (innerError) {
|
}
|
||||||
console.error('All shutdown methods failed:', innerError);
|
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 instanceof Error ? retryError.message : String(retryError),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 instanceof Error ? retryError.message : String(retryError),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 instanceof Error ? stdError.message : String(stdError),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine power status based on UPS model and raw value
|
||||||
|
* Uses the value mappings defined in the OID sets
|
||||||
|
* @param upsModel UPS model
|
||||||
|
* @param powerStatusValue Raw power status value
|
||||||
|
* @returns Standardized power status
|
||||||
|
*/
|
||||||
|
private determinePowerStatus(
|
||||||
|
upsModel: TUpsModel | undefined,
|
||||||
|
powerStatusValue: number,
|
||||||
|
): 'online' | 'onBattery' | 'unknown' {
|
||||||
|
// Get the OID set for this UPS model
|
||||||
|
if (upsModel && upsModel !== 'custom') {
|
||||||
|
const oidSet = UpsOidSets.getOidSet(upsModel);
|
||||||
|
|
||||||
|
// Use the value mappings if available
|
||||||
|
if (oidSet.POWER_STATUS_VALUES) {
|
||||||
|
if (powerStatusValue === oidSet.POWER_STATUS_VALUES.online) {
|
||||||
|
return 'online';
|
||||||
|
} else if (powerStatusValue === oidSet.POWER_STATUS_VALUES.onBattery) {
|
||||||
|
return 'onBattery';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for custom or undefined models (RFC 1628 standard)
|
||||||
|
// upsOutputSource: 3=normal (mains), 5=battery
|
||||||
|
if (powerStatusValue === 3) {
|
||||||
|
return 'online';
|
||||||
|
} else if (powerStatusValue === 5) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import type { OIDSet, UpsModel } from './types.js';
|
import type { IOidSet, TUpsModel } from './types.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OID sets for different UPS models
|
* OID sets for different UPS models
|
||||||
@@ -8,48 +8,68 @@ export class UpsOidSets {
|
|||||||
/**
|
/**
|
||||||
* OID sets for different UPS models
|
* OID sets for different UPS models
|
||||||
*/
|
*/
|
||||||
private static readonly UPS_OID_SETS: Record<UpsModel, OIDSet> = {
|
private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = {
|
||||||
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
|
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
|
||||||
cyberpower: {
|
cyberpower: {
|
||||||
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus (2=online, 3=on battery)
|
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
||||||
|
POWER_STATUS_VALUES: {
|
||||||
|
online: 2, // upsBaseOutputStatus: 2=onLine
|
||||||
|
onBattery: 3, // upsBaseOutputStatus: 3=onBattery
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// APC OIDs
|
// APC OIDs
|
||||||
apc: {
|
apc: {
|
||||||
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery)
|
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // upsBasicOutputStatus
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
||||||
|
POWER_STATUS_VALUES: {
|
||||||
|
online: 2, // upsBasicOutputStatus: 2=onLine
|
||||||
|
onBattery: 3, // upsBasicOutputStatus: 3=onBattery
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Eaton OIDs
|
// Eaton OIDs
|
||||||
eaton: {
|
eaton: {
|
||||||
POWER_STATUS: '1.3.6.1.4.1.534.1.1.2.0', // Power status
|
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // Battery capacity in percentage
|
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', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
|
||||||
|
POWER_STATUS_VALUES: {
|
||||||
|
online: 3, // xupsOutputSource: 3=normal (mains power)
|
||||||
|
onBattery: 5, // xupsOutputSource: 5=battery
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// TrippLite OIDs
|
// TrippLite OIDs
|
||||||
tripplite: {
|
tripplite: {
|
||||||
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status
|
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // tlUpsOutputSource
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
||||||
|
POWER_STATUS_VALUES: {
|
||||||
|
online: 2, // tlUpsOutputSource: 2=normal (mains power)
|
||||||
|
onBattery: 3, // tlUpsOutputSource: 3=onBattery
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Liebert/Vertiv OIDs
|
// Liebert/Vertiv OIDs
|
||||||
liebert: {
|
liebert: {
|
||||||
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status
|
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // lgpPwrOutputSource
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
||||||
|
POWER_STATUS_VALUES: {
|
||||||
|
online: 2, // lgpPwrOutputSource: 2=normal (mains power)
|
||||||
|
onBattery: 3, // lgpPwrOutputSource: 3=onBattery
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Custom OIDs (to be provided by the user)
|
// Custom OIDs (to be provided by the user)
|
||||||
custom: {
|
custom: {
|
||||||
POWER_STATUS: '',
|
POWER_STATUS: '',
|
||||||
BATTERY_CAPACITY: '',
|
BATTERY_CAPACITY: '',
|
||||||
BATTERY_RUNTIME: '',
|
BATTERY_RUNTIME: '',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +77,7 @@ export class UpsOidSets {
|
|||||||
* @param model UPS model name
|
* @param model UPS model name
|
||||||
* @returns OID set for the model
|
* @returns OID set for the model
|
||||||
*/
|
*/
|
||||||
public static getOidSet(model: UpsModel): OIDSet {
|
public static getOidSet(model: TUpsModel): IOidSet {
|
||||||
return this.UPS_OID_SETS[model];
|
return this.UPS_OID_SETS[model];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +87,9 @@ export class UpsOidSets {
|
|||||||
*/
|
*/
|
||||||
public static getStandardOids(): Record<string, string> {
|
public static getStandardOids(): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
||||||
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
||||||
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0' // upsEstimatedMinutesRemaining
|
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0', // upsEstimatedMinutesRemaining
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,651 +0,0 @@
|
|||||||
import * as crypto from 'crypto';
|
|
||||||
import type { SnmpConfig, SnmpV3SecurityParams } from './types.js';
|
|
||||||
import { SnmpEncoder } from './encoder.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SNMP packet creation utilities
|
|
||||||
* Creates SNMP request packets for different SNMP versions
|
|
||||||
*/
|
|
||||||
export class SnmpPacketCreator {
|
|
||||||
/**
|
|
||||||
* Create an SNMPv1 GET request
|
|
||||||
* @param oid OID to query
|
|
||||||
* @param community Community string
|
|
||||||
* @param debug Whether to enable debug output
|
|
||||||
* @returns Buffer containing the SNMP request
|
|
||||||
*/
|
|
||||||
public static createSnmpGetRequest(oid: string, community: string, debug: boolean = false): Buffer {
|
|
||||||
const oidArray = SnmpEncoder.oidToArray(oid);
|
|
||||||
const encodedOid = SnmpEncoder.encodeOID(oidArray);
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('OID array length:', oidArray.length);
|
|
||||||
console.log('OID array:', oidArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SNMP message structure
|
|
||||||
// Sequence
|
|
||||||
// Version (Integer)
|
|
||||||
// Community (String)
|
|
||||||
// PDU (GetRequest)
|
|
||||||
// Request ID (Integer)
|
|
||||||
// Error Status (Integer)
|
|
||||||
// Error Index (Integer)
|
|
||||||
// Variable Bindings (Sequence)
|
|
||||||
// Variable (Sequence)
|
|
||||||
// OID (ObjectIdentifier)
|
|
||||||
// Value (Null)
|
|
||||||
|
|
||||||
// Use the standard method from our test that is known to work
|
|
||||||
// Create a fixed request ID (0x00000001) to ensure deterministic behavior
|
|
||||||
const requestId = Buffer.from([0x00, 0x00, 0x00, 0x01]);
|
|
||||||
|
|
||||||
// Encode values
|
|
||||||
const versionBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x00]) // SNMP version 1 (0)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const communityBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, community.length]), // ASN.1 Octet String, length
|
|
||||||
Buffer.from(community) // Community string
|
|
||||||
]);
|
|
||||||
|
|
||||||
const requestIdBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
|
||||||
requestId // Fixed Request ID
|
|
||||||
]);
|
|
||||||
|
|
||||||
const errorStatusBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x00]) // Error Status (0 = no error)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const errorIndexBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x00]) // Error Index (0)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const oidValueBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([encodedOid.length + 2]), // Length
|
|
||||||
Buffer.from([0x06]), // ASN.1 Object Identifier
|
|
||||||
Buffer.from([encodedOid.length]), // Length
|
|
||||||
encodedOid, // OID
|
|
||||||
Buffer.from([0x05, 0x00]) // Null value
|
|
||||||
]);
|
|
||||||
|
|
||||||
const varBindingsBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([oidValueBuf.length]), // Length
|
|
||||||
oidValueBuf // Variable binding
|
|
||||||
]);
|
|
||||||
|
|
||||||
const pduBuf = Buffer.concat([
|
|
||||||
Buffer.from([0xa0]), // ASN.1 Context-specific Constructed 0 (GetRequest)
|
|
||||||
Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length
|
|
||||||
requestIdBuf, // Request ID
|
|
||||||
errorStatusBuf, // Error Status
|
|
||||||
errorIndexBuf, // Error Index
|
|
||||||
varBindingsBuf // Variable Bindings
|
|
||||||
]);
|
|
||||||
|
|
||||||
const messageBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([versionBuf.length + communityBuf.length + pduBuf.length]), // Length
|
|
||||||
versionBuf, // Version
|
|
||||||
communityBuf, // Community
|
|
||||||
pduBuf // PDU
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('SNMP Request buffer:', messageBuf.toString('hex'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageBuf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an SNMPv3 GET request
|
|
||||||
* @param oid OID to query
|
|
||||||
* @param config SNMP configuration
|
|
||||||
* @param engineID Engine ID
|
|
||||||
* @param engineBoots Engine boots counter
|
|
||||||
* @param engineTime Engine time counter
|
|
||||||
* @param requestID Request ID
|
|
||||||
* @param debug Whether to enable debug output
|
|
||||||
* @returns Buffer containing the SNMP request
|
|
||||||
*/
|
|
||||||
public static createSnmpV3GetRequest(
|
|
||||||
oid: string,
|
|
||||||
config: SnmpConfig,
|
|
||||||
engineID: Buffer,
|
|
||||||
engineBoots: number,
|
|
||||||
engineTime: number,
|
|
||||||
requestID: number,
|
|
||||||
debug: boolean = false
|
|
||||||
): Buffer {
|
|
||||||
if (debug) {
|
|
||||||
console.log('Creating SNMPv3 GET request for OID:', oid);
|
|
||||||
console.log('With config:', {
|
|
||||||
...config,
|
|
||||||
authKey: config.authKey ? '***' : undefined,
|
|
||||||
privKey: config.privKey ? '***' : undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const oidArray = SnmpEncoder.oidToArray(oid);
|
|
||||||
const encodedOid = SnmpEncoder.encodeOID(oidArray);
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('Using engine ID:', engineID.toString('hex'));
|
|
||||||
console.log('Engine boots:', engineBoots);
|
|
||||||
console.log('Engine time:', engineTime);
|
|
||||||
console.log('Request ID:', requestID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create security parameters
|
|
||||||
const securityParams: SnmpV3SecurityParams = {
|
|
||||||
msgAuthoritativeEngineID: engineID,
|
|
||||||
msgAuthoritativeEngineBoots: engineBoots,
|
|
||||||
msgAuthoritativeEngineTime: engineTime,
|
|
||||||
msgUserName: config.username || '',
|
|
||||||
msgAuthenticationParameters: Buffer.alloc(12, 0), // Will be filled in later for auth
|
|
||||||
msgPrivacyParameters: Buffer.alloc(8, 0), // For privacy
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the PDU (Protocol Data Unit)
|
|
||||||
// This is wrapped within the security parameters
|
|
||||||
const requestIdBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
|
||||||
SnmpEncoder.encodeInteger(requestID) // Request ID
|
|
||||||
]);
|
|
||||||
|
|
||||||
const errorStatusBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x00]) // Error Status (0 = no error)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const errorIndexBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x00]) // Error Index (0)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const oidValueBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([encodedOid.length + 2]), // Length
|
|
||||||
Buffer.from([0x06]), // ASN.1 Object Identifier
|
|
||||||
Buffer.from([encodedOid.length]), // Length
|
|
||||||
encodedOid, // OID
|
|
||||||
Buffer.from([0x05, 0x00]) // Null value
|
|
||||||
]);
|
|
||||||
|
|
||||||
const varBindingsBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([oidValueBuf.length]), // Length
|
|
||||||
oidValueBuf // Variable binding
|
|
||||||
]);
|
|
||||||
|
|
||||||
const pduBuf = Buffer.concat([
|
|
||||||
Buffer.from([0xa0]), // ASN.1 Context-specific Constructed 0 (GetRequest)
|
|
||||||
Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length
|
|
||||||
requestIdBuf, // Request ID
|
|
||||||
errorStatusBuf, // Error Status
|
|
||||||
errorIndexBuf, // Error Index
|
|
||||||
varBindingsBuf // Variable Bindings
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create the security parameters
|
|
||||||
const engineIdBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, securityParams.msgAuthoritativeEngineID.length]), // ASN.1 Octet String
|
|
||||||
securityParams.msgAuthoritativeEngineID
|
|
||||||
]);
|
|
||||||
|
|
||||||
const engineBootsBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
|
||||||
SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineBoots)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const engineTimeBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
|
||||||
SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineTime)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const userNameBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, securityParams.msgUserName.length]), // ASN.1 Octet String
|
|
||||||
Buffer.from(securityParams.msgUserName)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const authParamsBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, securityParams.msgAuthenticationParameters.length]), // ASN.1 Octet String
|
|
||||||
securityParams.msgAuthenticationParameters
|
|
||||||
]);
|
|
||||||
|
|
||||||
const privParamsBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, securityParams.msgPrivacyParameters.length]), // ASN.1 Octet String
|
|
||||||
securityParams.msgPrivacyParameters
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Security parameters sequence
|
|
||||||
const securityParamsBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([engineIdBuf.length + engineBootsBuf.length + engineTimeBuf.length +
|
|
||||||
userNameBuf.length + authParamsBuf.length + privParamsBuf.length]), // Length
|
|
||||||
engineIdBuf,
|
|
||||||
engineBootsBuf,
|
|
||||||
engineTimeBuf,
|
|
||||||
userNameBuf,
|
|
||||||
authParamsBuf,
|
|
||||||
privParamsBuf
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Determine security level flags
|
|
||||||
let securityFlags = 0;
|
|
||||||
if (config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') {
|
|
||||||
securityFlags |= 0x01; // Authentication flag
|
|
||||||
}
|
|
||||||
if (config.securityLevel === 'authPriv') {
|
|
||||||
securityFlags |= 0x02; // Privacy flag
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set reportable flag - required for SNMPv3
|
|
||||||
securityFlags |= 0x04; // Reportable flag
|
|
||||||
|
|
||||||
// Create SNMPv3 header
|
|
||||||
const msgIdBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
|
||||||
SnmpEncoder.encodeInteger(requestID) // Message ID (same as request ID for simplicity)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const msgMaxSizeBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
|
||||||
SnmpEncoder.encodeInteger(65507) // Max message size
|
|
||||||
]);
|
|
||||||
|
|
||||||
const msgFlagsBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1
|
|
||||||
Buffer.from([securityFlags])
|
|
||||||
]);
|
|
||||||
|
|
||||||
const msgSecModelBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x03]) // Security model (3 = USM)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// SNMPv3 header
|
|
||||||
const msgHeaderBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length
|
|
||||||
msgIdBuf,
|
|
||||||
msgMaxSizeBuf,
|
|
||||||
msgFlagsBuf,
|
|
||||||
msgSecModelBuf
|
|
||||||
]);
|
|
||||||
|
|
||||||
// SNMPv3 security parameters
|
|
||||||
const msgSecurityBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04]), // ASN.1 Octet String
|
|
||||||
Buffer.from([securityParamsBuf.length]), // Length
|
|
||||||
securityParamsBuf
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Create scopedPDU
|
|
||||||
// In SNMPv3, the PDU is wrapped in a "scoped PDU" structure
|
|
||||||
const contextEngineBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, engineID.length]), // ASN.1 Octet String
|
|
||||||
engineID
|
|
||||||
]);
|
|
||||||
|
|
||||||
const contextNameBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, 0x00]), // ASN.1 Octet String, length 0 (empty context name)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const scopedPduBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]), // Length
|
|
||||||
contextEngineBuf,
|
|
||||||
contextNameBuf,
|
|
||||||
pduBuf
|
|
||||||
]);
|
|
||||||
|
|
||||||
// For authPriv, we need to encrypt the scopedPDU
|
|
||||||
let encryptedPdu = scopedPduBuf;
|
|
||||||
if (config.securityLevel === 'authPriv' && config.privKey) {
|
|
||||||
// In a real implementation, encryption would be applied here
|
|
||||||
// For this example, we'll just simulate it
|
|
||||||
encryptedPdu = this.simulateEncryption(scopedPduBuf, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final scopedPDU (encrypted or not)
|
|
||||||
const finalScopedPduBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04]), // ASN.1 Octet String
|
|
||||||
Buffer.from([encryptedPdu.length]), // Length
|
|
||||||
encryptedPdu
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Combine everything for the final message
|
|
||||||
const versionBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x03]) // SNMP version 3 (3)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const messageBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([versionBuf.length + msgHeaderBuf.length + msgSecurityBuf.length + finalScopedPduBuf.length]), // Length
|
|
||||||
versionBuf,
|
|
||||||
msgHeaderBuf,
|
|
||||||
msgSecurityBuf,
|
|
||||||
finalScopedPduBuf
|
|
||||||
]);
|
|
||||||
|
|
||||||
// If using authentication, calculate and insert the authentication parameters
|
|
||||||
if ((config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') &&
|
|
||||||
config.authKey && config.authProtocol) {
|
|
||||||
const authenticatedMsg = this.addAuthentication(messageBuf, config, authParamsBuf);
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('Created authenticated SNMPv3 message');
|
|
||||||
console.log('Final message length:', authenticatedMsg.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return authenticatedMsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('Created SNMPv3 message without authentication');
|
|
||||||
console.log('Final message length:', messageBuf.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageBuf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulate encryption for authPriv security level
|
|
||||||
* In a real implementation, this would use the specified privacy protocol (DES/AES)
|
|
||||||
* @param data Data to encrypt
|
|
||||||
* @param config SNMP configuration
|
|
||||||
* @returns Encrypted data
|
|
||||||
*/
|
|
||||||
private static simulateEncryption(data: Buffer, config: SnmpConfig): Buffer {
|
|
||||||
// This is a placeholder - in a real implementation, you would:
|
|
||||||
// 1. Generate an initialization vector (IV)
|
|
||||||
// 2. Use the privacy key derived from the privKey
|
|
||||||
// 3. Apply the appropriate encryption algorithm (DES/AES)
|
|
||||||
|
|
||||||
// For demonstration purposes only
|
|
||||||
if (config.privProtocol === 'AES' && config.privKey) {
|
|
||||||
try {
|
|
||||||
// Create a deterministic IV for demo purposes (not secure for production)
|
|
||||||
const iv = Buffer.alloc(16, 0);
|
|
||||||
const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
iv[i] = engineID[i % engineID.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a key from the privKey (proper key localization should be used in production)
|
|
||||||
const key = crypto.createHash('md5').update(config.privKey).digest();
|
|
||||||
|
|
||||||
// Create cipher and encrypt
|
|
||||||
const cipher = crypto.createCipheriv('aes-128-cfb', key, iv);
|
|
||||||
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
||||||
|
|
||||||
return encrypted;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('AES encryption failed, falling back to plaintext:', error);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
} else if (config.privProtocol === 'DES' && config.privKey) {
|
|
||||||
try {
|
|
||||||
// Create a deterministic IV for demo purposes (not secure for production)
|
|
||||||
const iv = Buffer.alloc(8, 0);
|
|
||||||
const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
iv[i] = engineID[i % engineID.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a key from the privKey (proper key localization should be used in production)
|
|
||||||
const key = crypto.createHash('md5').update(config.privKey).digest().slice(0, 8);
|
|
||||||
|
|
||||||
// Create cipher and encrypt
|
|
||||||
const cipher = crypto.createCipheriv('des-cbc', key, iv);
|
|
||||||
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
||||||
|
|
||||||
return encrypted;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('DES encryption failed, falling back to plaintext:', error);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data; // Return unencrypted data as fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add authentication to SNMPv3 message
|
|
||||||
* @param message Message to authenticate
|
|
||||||
* @param config SNMP configuration
|
|
||||||
* @param authParamsBuf Authentication parameters buffer
|
|
||||||
* @returns Authenticated message
|
|
||||||
*/
|
|
||||||
private static addAuthentication(message: Buffer, config: SnmpConfig, authParamsBuf: Buffer): Buffer {
|
|
||||||
// In a real implementation, this would:
|
|
||||||
// 1. Zero out the authentication parameters field
|
|
||||||
// 2. Calculate HMAC-MD5 or HMAC-SHA1 over the entire message
|
|
||||||
// 3. Insert the HMAC into the authentication parameters field
|
|
||||||
|
|
||||||
if (!config.authKey) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find position of auth parameters in the message
|
|
||||||
// This is a more reliable way to find the exact position
|
|
||||||
let authParamsPos = -1;
|
|
||||||
for (let i = 0; i < message.length - 16; i++) {
|
|
||||||
// Look for the auth params pattern: 0x04 0x0C 0x00 0x00...
|
|
||||||
if (message[i] === 0x04 && message[i + 1] === 0x0C) {
|
|
||||||
// Check if next 12 bytes are all zeros
|
|
||||||
let allZeros = true;
|
|
||||||
for (let j = 0; j < 12; j++) {
|
|
||||||
if (message[i + 2 + j] !== 0) {
|
|
||||||
allZeros = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (allZeros) {
|
|
||||||
authParamsPos = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authParamsPos === -1) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a copy of the message with zeroed auth parameters
|
|
||||||
const msgCopy = Buffer.from(message);
|
|
||||||
|
|
||||||
// Prepare the authentication key according to RFC3414
|
|
||||||
// We should use the standard key localization process
|
|
||||||
const localizedKey = this.localizeAuthKey(config.authKey,
|
|
||||||
Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]),
|
|
||||||
config.authProtocol);
|
|
||||||
|
|
||||||
// Calculate HMAC
|
|
||||||
let hmac;
|
|
||||||
if (config.authProtocol === 'SHA') {
|
|
||||||
hmac = crypto.createHmac('sha1', localizedKey).update(msgCopy).digest().slice(0, 12);
|
|
||||||
} else {
|
|
||||||
// Default to MD5
|
|
||||||
hmac = crypto.createHmac('md5', localizedKey).update(msgCopy).digest().slice(0, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy HMAC into original message
|
|
||||||
hmac.copy(message, authParamsPos + 2);
|
|
||||||
|
|
||||||
return message;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Authentication failed:', error);
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Localize authentication key according to RFC3414
|
|
||||||
* @param key Authentication key
|
|
||||||
* @param engineId Engine ID
|
|
||||||
* @param authProtocol Authentication protocol
|
|
||||||
* @returns Localized key
|
|
||||||
*/
|
|
||||||
private static localizeAuthKey(key: string, engineId: Buffer, authProtocol: string = 'MD5'): Buffer {
|
|
||||||
try {
|
|
||||||
// Convert password to key using hash
|
|
||||||
let initialHash;
|
|
||||||
if (authProtocol === 'SHA') {
|
|
||||||
initialHash = crypto.createHash('sha1');
|
|
||||||
} else {
|
|
||||||
initialHash = crypto.createHash('md5');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the initial key - repeated hashing of password + padding
|
|
||||||
const password = Buffer.from(key);
|
|
||||||
let passwordIndex = 0;
|
|
||||||
|
|
||||||
// Create a buffer of 1MB (1048576 bytes) filled with the password
|
|
||||||
const buffer = Buffer.alloc(1048576);
|
|
||||||
for (let i = 0; i < 1048576; i++) {
|
|
||||||
buffer[i] = password[passwordIndex];
|
|
||||||
passwordIndex = (passwordIndex + 1) % password.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialHash.update(buffer);
|
|
||||||
let initialKey = initialHash.digest();
|
|
||||||
|
|
||||||
// Localize the key with engine ID
|
|
||||||
let localHash;
|
|
||||||
if (authProtocol === 'SHA') {
|
|
||||||
localHash = crypto.createHash('sha1');
|
|
||||||
} else {
|
|
||||||
localHash = crypto.createHash('md5');
|
|
||||||
}
|
|
||||||
|
|
||||||
localHash.update(initialKey);
|
|
||||||
localHash.update(engineId);
|
|
||||||
localHash.update(initialKey);
|
|
||||||
|
|
||||||
return localHash.digest();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error localizing auth key:', error);
|
|
||||||
// Return a fallback key
|
|
||||||
return Buffer.from(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a discovery message for SNMPv3 engine ID discovery
|
|
||||||
* @param config SNMP configuration
|
|
||||||
* @param requestID Request ID
|
|
||||||
* @returns Discovery message
|
|
||||||
*/
|
|
||||||
public static createDiscoveryMessage(config: SnmpConfig, requestID: number): Buffer {
|
|
||||||
// Basic SNMPv3 header for discovery
|
|
||||||
const msgIdBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
|
||||||
SnmpEncoder.encodeInteger(requestID)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const msgMaxSizeBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
|
||||||
SnmpEncoder.encodeInteger(65507) // Max message size
|
|
||||||
]);
|
|
||||||
|
|
||||||
const msgFlagsBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1
|
|
||||||
Buffer.from([0x00]) // No authentication or privacy
|
|
||||||
]);
|
|
||||||
|
|
||||||
const msgSecModelBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x03]) // Security model (3 = USM)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// SNMPv3 header
|
|
||||||
const msgHeaderBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]), // ASN.1 Sequence
|
|
||||||
Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length
|
|
||||||
msgIdBuf,
|
|
||||||
msgMaxSizeBuf,
|
|
||||||
msgFlagsBuf,
|
|
||||||
msgSecModelBuf
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Simple security parameters for discovery
|
|
||||||
const securityBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, 0x00]), // Empty octet string
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Simple Get request for discovery
|
|
||||||
const requestIdBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4
|
|
||||||
SnmpEncoder.encodeInteger(requestID + 1)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const errorStatusBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x00]) // Error Status (0 = no error)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const errorIndexBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x00]) // Error Index (0)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Empty varbinds for discovery
|
|
||||||
const varBindingsBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30, 0x00]), // Empty sequence
|
|
||||||
]);
|
|
||||||
|
|
||||||
const pduBuf = Buffer.concat([
|
|
||||||
Buffer.from([0xa0]), // GetRequest
|
|
||||||
Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]),
|
|
||||||
requestIdBuf,
|
|
||||||
errorStatusBuf,
|
|
||||||
errorIndexBuf,
|
|
||||||
varBindingsBuf
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Context data
|
|
||||||
const contextEngineBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, 0x00]), // Empty octet string
|
|
||||||
]);
|
|
||||||
|
|
||||||
const contextNameBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x04, 0x00]), // Empty octet string
|
|
||||||
]);
|
|
||||||
|
|
||||||
const scopedPduBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x30]),
|
|
||||||
Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]),
|
|
||||||
contextEngineBuf,
|
|
||||||
contextNameBuf,
|
|
||||||
pduBuf
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Version
|
|
||||||
const versionBuf = Buffer.concat([
|
|
||||||
Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1
|
|
||||||
Buffer.from([0x03]) // SNMP version 3 (3)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Complete message
|
|
||||||
return Buffer.concat([
|
|
||||||
Buffer.from([0x30]),
|
|
||||||
Buffer.from([versionBuf.length + msgHeaderBuf.length + securityBuf.length + scopedPduBuf.length]),
|
|
||||||
versionBuf,
|
|
||||||
msgHeaderBuf,
|
|
||||||
securityBuf,
|
|
||||||
scopedPduBuf
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,553 +0,0 @@
|
|||||||
import type { SnmpConfig } from './types.js';
|
|
||||||
import { SnmpEncoder } from './encoder.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SNMP packet parsing utilities
|
|
||||||
* Parses SNMP response packets
|
|
||||||
*/
|
|
||||||
export class SnmpPacketParser {
|
|
||||||
/**
|
|
||||||
* Parse an SNMP response
|
|
||||||
* @param buffer Response buffer
|
|
||||||
* @param config SNMP configuration
|
|
||||||
* @param debug Whether to enable debug output
|
|
||||||
* @returns Parsed value or null if parsing failed
|
|
||||||
*/
|
|
||||||
public static parseSnmpResponse(buffer: Buffer, config: SnmpConfig, debug: boolean = false): any {
|
|
||||||
// Check if we have a response packet
|
|
||||||
if (buffer[0] !== 0x30) {
|
|
||||||
throw new Error('Invalid SNMP response format');
|
|
||||||
}
|
|
||||||
|
|
||||||
// For SNMPv3, we need to handle the message differently
|
|
||||||
if (config.version === 3) {
|
|
||||||
return this.parseSnmpV3Response(buffer, debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('Parsing SNMPv1/v2 response: ', buffer.toString('hex'));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Enhanced structured parsing approach
|
|
||||||
// SEQUENCE header
|
|
||||||
let pos = 0;
|
|
||||||
if (buffer[pos] !== 0x30) {
|
|
||||||
throw new Error('Missing SEQUENCE at start of response');
|
|
||||||
}
|
|
||||||
// Skip SEQUENCE header - assume length is in single byte for simplicity
|
|
||||||
// In a more robust implementation, we'd handle multi-byte lengths
|
|
||||||
pos += 2;
|
|
||||||
|
|
||||||
// VERSION
|
|
||||||
if (buffer[pos] !== 0x02) {
|
|
||||||
throw new Error('Missing INTEGER for version');
|
|
||||||
}
|
|
||||||
const versionLength = buffer[pos + 1];
|
|
||||||
pos += 2 + versionLength;
|
|
||||||
|
|
||||||
// COMMUNITY STRING
|
|
||||||
if (buffer[pos] !== 0x04) {
|
|
||||||
throw new Error('Missing OCTET STRING for community');
|
|
||||||
}
|
|
||||||
const communityLength = buffer[pos + 1];
|
|
||||||
pos += 2 + communityLength;
|
|
||||||
|
|
||||||
// PDU TYPE - should be RESPONSE (0xA2)
|
|
||||||
if (buffer[pos] !== 0xA2) {
|
|
||||||
throw new Error(`Unexpected PDU type: 0x${buffer[pos].toString(16)}, expected 0xA2`);
|
|
||||||
}
|
|
||||||
// Skip PDU header
|
|
||||||
pos += 2;
|
|
||||||
|
|
||||||
// REQUEST ID
|
|
||||||
if (buffer[pos] !== 0x02) {
|
|
||||||
throw new Error('Missing INTEGER for request ID');
|
|
||||||
}
|
|
||||||
const requestIdLength = buffer[pos + 1];
|
|
||||||
pos += 2 + requestIdLength;
|
|
||||||
|
|
||||||
// ERROR STATUS
|
|
||||||
if (buffer[pos] !== 0x02) {
|
|
||||||
throw new Error('Missing INTEGER for error status');
|
|
||||||
}
|
|
||||||
const errorStatusLength = buffer[pos + 1];
|
|
||||||
const errorStatus = SnmpEncoder.decodeInteger(buffer, pos + 2, errorStatusLength);
|
|
||||||
|
|
||||||
if (errorStatus !== 0) {
|
|
||||||
throw new Error(`SNMP error status: ${errorStatus}`);
|
|
||||||
}
|
|
||||||
pos += 2 + errorStatusLength;
|
|
||||||
|
|
||||||
// ERROR INDEX
|
|
||||||
if (buffer[pos] !== 0x02) {
|
|
||||||
throw new Error('Missing INTEGER for error index');
|
|
||||||
}
|
|
||||||
const errorIndexLength = buffer[pos + 1];
|
|
||||||
pos += 2 + errorIndexLength;
|
|
||||||
|
|
||||||
// VARBIND LIST
|
|
||||||
if (buffer[pos] !== 0x30) {
|
|
||||||
throw new Error('Missing SEQUENCE for varbind list');
|
|
||||||
}
|
|
||||||
// Skip varbind list header
|
|
||||||
pos += 2;
|
|
||||||
|
|
||||||
// VARBIND
|
|
||||||
if (buffer[pos] !== 0x30) {
|
|
||||||
throw new Error('Missing SEQUENCE for varbind');
|
|
||||||
}
|
|
||||||
// Skip varbind header
|
|
||||||
pos += 2;
|
|
||||||
|
|
||||||
// OID
|
|
||||||
if (buffer[pos] !== 0x06) {
|
|
||||||
throw new Error('Missing OBJECT IDENTIFIER for OID');
|
|
||||||
}
|
|
||||||
const oidLength = buffer[pos + 1];
|
|
||||||
pos += 2 + oidLength;
|
|
||||||
|
|
||||||
// VALUE - this is what we want
|
|
||||||
const valueType = buffer[pos];
|
|
||||||
const valueLength = buffer[pos + 1];
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log(`Found value type: 0x${valueType.toString(16)}, length: ${valueLength}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.parseValueByType(valueType, valueLength, buffer, pos, debug);
|
|
||||||
} catch (error) {
|
|
||||||
if (debug) {
|
|
||||||
console.error('Error in structured parsing:', error);
|
|
||||||
console.error('Falling back to scan-based parsing method');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.scanBasedParsing(buffer, debug);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse value by ASN.1 type
|
|
||||||
* @param valueType ASN.1 type
|
|
||||||
* @param valueLength Value length
|
|
||||||
* @param buffer Buffer containing the value
|
|
||||||
* @param pos Position of the value in the buffer
|
|
||||||
* @param debug Whether to enable debug output
|
|
||||||
* @returns Parsed value
|
|
||||||
*/
|
|
||||||
private static parseValueByType(
|
|
||||||
valueType: number,
|
|
||||||
valueLength: number,
|
|
||||||
buffer: Buffer,
|
|
||||||
pos: number,
|
|
||||||
debug: boolean
|
|
||||||
): any {
|
|
||||||
switch (valueType) {
|
|
||||||
case 0x02: // INTEGER
|
|
||||||
{
|
|
||||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
|
||||||
if (debug) {
|
|
||||||
console.log('Parsed INTEGER value:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 0x04: // OCTET STRING
|
|
||||||
{
|
|
||||||
const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString();
|
|
||||||
if (debug) {
|
|
||||||
console.log('Parsed OCTET STRING value:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 0x05: // NULL
|
|
||||||
if (debug) {
|
|
||||||
console.log('Parsed NULL value');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case 0x06: // OBJECT IDENTIFIER (rare in a value position)
|
|
||||||
{
|
|
||||||
// Usually this would be encoded as a string representation
|
|
||||||
const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString('hex');
|
|
||||||
if (debug) {
|
|
||||||
console.log('Parsed OBJECT IDENTIFIER value (hex):', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 0x40: // IP ADDRESS
|
|
||||||
{
|
|
||||||
if (valueLength !== 4) {
|
|
||||||
throw new Error(`Invalid IP address length: ${valueLength}, expected 4`);
|
|
||||||
}
|
|
||||||
const octets = [];
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
octets.push(buffer[pos + 2 + i]);
|
|
||||||
}
|
|
||||||
const value = octets.join('.');
|
|
||||||
if (debug) {
|
|
||||||
console.log('Parsed IP ADDRESS value:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 0x41: // COUNTER
|
|
||||||
case 0x42: // GAUGE32
|
|
||||||
case 0x43: // TIMETICKS
|
|
||||||
case 0x44: // OPAQUE
|
|
||||||
{
|
|
||||||
// All these are essentially unsigned 32-bit integers
|
|
||||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
|
||||||
if (debug) {
|
|
||||||
console.log(`Parsed ${valueType === 0x41 ? 'COUNTER'
|
|
||||||
: valueType === 0x42 ? 'GAUGE32'
|
|
||||||
: valueType === 0x43 ? 'TIMETICKS'
|
|
||||||
: 'OPAQUE'} value:`, value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (debug) {
|
|
||||||
console.log(`Unknown value type: 0x${valueType.toString(16)}`);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback scan-based parsing method
|
|
||||||
* @param buffer Buffer containing the SNMP response
|
|
||||||
* @param debug Whether to enable debug output
|
|
||||||
* @returns Parsed value or null if parsing failed
|
|
||||||
*/
|
|
||||||
private static scanBasedParsing(buffer: Buffer, debug: boolean): any {
|
|
||||||
// Look for various data types in the response
|
|
||||||
// The value is near the end of the packet after the OID
|
|
||||||
|
|
||||||
// We're looking for one of these:
|
|
||||||
// 0x02 - Integer - can be at the end of a varbind
|
|
||||||
// 0x04 - OctetString
|
|
||||||
// 0x05 - Null
|
|
||||||
// 0x42 - Gauge32 - special type for unsigned 32-bit integers
|
|
||||||
// 0x43 - Timeticks - special type for time values
|
|
||||||
|
|
||||||
// This algorithm performs a thorough search for data types
|
|
||||||
// by iterating from the start and watching for varbind structures
|
|
||||||
|
|
||||||
// Walk through the buffer looking for varbinds
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
// First, find the varbinds section (0x30 sequence)
|
|
||||||
while (i < buffer.length - 2) {
|
|
||||||
// Look for a varbinds sequence
|
|
||||||
if (buffer[i] === 0x30) {
|
|
||||||
const varbindsLength = buffer[i + 1];
|
|
||||||
const varbindsEnd = i + 2 + varbindsLength;
|
|
||||||
|
|
||||||
// Now search within the varbinds for the value
|
|
||||||
let j = i + 2;
|
|
||||||
while (j < varbindsEnd - 2) {
|
|
||||||
// Look for a varbind (0x30 sequence)
|
|
||||||
if (buffer[j] === 0x30) {
|
|
||||||
const varbindLength = buffer[j + 1];
|
|
||||||
const varbindEnd = j + 2 + varbindLength;
|
|
||||||
|
|
||||||
// Skip over the OID and find the value within this varbind
|
|
||||||
let k = j + 2;
|
|
||||||
while (k < varbindEnd - 1) {
|
|
||||||
// First find the OID
|
|
||||||
if (buffer[k] === 0x06) { // OID
|
|
||||||
const oidLength = buffer[k + 1];
|
|
||||||
k += 2 + oidLength; // Skip past the OID
|
|
||||||
|
|
||||||
// We should now be at the value
|
|
||||||
// Check what type it is
|
|
||||||
if (k < varbindEnd - 1) {
|
|
||||||
return this.parseValueAtPosition(buffer, k, debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't find a value, move to next byte
|
|
||||||
k++;
|
|
||||||
} else {
|
|
||||||
// Move to next byte
|
|
||||||
k++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to next varbind
|
|
||||||
j = varbindEnd;
|
|
||||||
} else {
|
|
||||||
// Move to next byte
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to next sequence
|
|
||||||
i = varbindsEnd;
|
|
||||||
} else {
|
|
||||||
// Move to next byte
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('No valid value found in SNMP response');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse value at a specific position in the buffer
|
|
||||||
* @param buffer Buffer containing the SNMP response
|
|
||||||
* @param pos Position of the value in the buffer
|
|
||||||
* @param debug Whether to enable debug output
|
|
||||||
* @returns Parsed value or null if parsing failed
|
|
||||||
*/
|
|
||||||
private static parseValueAtPosition(buffer: Buffer, pos: number, debug: boolean): any {
|
|
||||||
if (buffer[pos] === 0x02) { // Integer
|
|
||||||
const valueLength = buffer[pos + 1];
|
|
||||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found Integer value:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} else if (buffer[pos] === 0x42) { // Gauge32
|
|
||||||
const valueLength = buffer[pos + 1];
|
|
||||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found Gauge32 value:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} else if (buffer[pos] === 0x43) { // TimeTicks
|
|
||||||
const valueLength = buffer[pos + 1];
|
|
||||||
const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength);
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found Timeticks value:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} else if (buffer[pos] === 0x04) { // OctetString
|
|
||||||
const valueLength = buffer[pos + 1];
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found OctetString value');
|
|
||||||
}
|
|
||||||
// Just return the string value as-is
|
|
||||||
return buffer.slice(pos + 2, pos + 2 + valueLength).toString();
|
|
||||||
} else if (buffer[pos] === 0x05) { // Null
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found Null value');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse an SNMPv3 response
|
|
||||||
* @param buffer Buffer containing the SNMP response
|
|
||||||
* @param debug Whether to enable debug output
|
|
||||||
* @returns Parsed value or null if parsing failed
|
|
||||||
*/
|
|
||||||
public static parseSnmpV3Response(buffer: Buffer, debug: boolean = false): any {
|
|
||||||
// SNMPv3 parsing is complex. In a real implementation, we would:
|
|
||||||
// 1. Parse the header and get the security parameters
|
|
||||||
// 2. Verify authentication if used
|
|
||||||
// 3. Decrypt the PDU if privacy was used
|
|
||||||
// 4. Extract the PDU and parse it
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('Parsing SNMPv3 response: ', buffer.toString('hex'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the scopedPDU - it should be the last OCTET STRING in the message
|
|
||||||
let scopedPduPos = -1;
|
|
||||||
for (let i = buffer.length - 50; i >= 0; i--) {
|
|
||||||
if (buffer[i] === 0x04 && buffer[i + 1] > 10) { // OCTET STRING with reasonable length
|
|
||||||
scopedPduPos = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scopedPduPos === -1) {
|
|
||||||
if (debug) {
|
|
||||||
console.log('Could not find scoped PDU in SNMPv3 response');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip to the PDU content
|
|
||||||
let pduContent = buffer.slice(scopedPduPos + 2); // Skip OCTET STRING header
|
|
||||||
|
|
||||||
// This improved algorithm performs a more thorough search for varbinds
|
|
||||||
// in the scoped PDU
|
|
||||||
|
|
||||||
// First, look for the response PDU (sequence with tag 0xa2)
|
|
||||||
let responsePdu = null;
|
|
||||||
for (let i = 0; i < pduContent.length - 3; i++) {
|
|
||||||
if (pduContent[i] === 0xa2) {
|
|
||||||
// Found the response PDU
|
|
||||||
const pduLength = pduContent[i + 1];
|
|
||||||
responsePdu = pduContent.slice(i, i + 2 + pduLength);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responsePdu) {
|
|
||||||
// Try to find the varbinds directly
|
|
||||||
for (let i = 0; i < pduContent.length - 3; i++) {
|
|
||||||
if (pduContent[i] === 0x30) {
|
|
||||||
const seqLength = pduContent[i + 1];
|
|
||||||
if (i + 2 + seqLength <= pduContent.length) {
|
|
||||||
// Check if this sequence might be the varbinds
|
|
||||||
const possibleVarbinds = pduContent.slice(i, i + 2 + seqLength);
|
|
||||||
|
|
||||||
// Look for varbind structure inside
|
|
||||||
for (let j = 0; j < possibleVarbinds.length - 3; j++) {
|
|
||||||
if (possibleVarbinds[j] === 0x30) {
|
|
||||||
// Might be a varbind - look for an OID inside
|
|
||||||
for (let k = j; k < j + 10 && k < possibleVarbinds.length - 1; k++) {
|
|
||||||
if (possibleVarbinds[k] === 0x06) {
|
|
||||||
// Found an OID, so this is likely the varbinds sequence
|
|
||||||
responsePdu = possibleVarbinds;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responsePdu) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responsePdu) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responsePdu) {
|
|
||||||
if (debug) {
|
|
||||||
console.log('Could not find response PDU in SNMPv3 response');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have the response PDU, search for varbinds
|
|
||||||
// Skip the first few bytes to get past the header fields
|
|
||||||
let varbindsPos = -1;
|
|
||||||
for (let i = 10; i < responsePdu.length - 3; i++) {
|
|
||||||
if (responsePdu[i] === 0x30) {
|
|
||||||
// Check if this is the start of the varbinds
|
|
||||||
// by seeing if it contains a varbind sequence
|
|
||||||
for (let j = i + 2; j < i + 10 && j < responsePdu.length - 3; j++) {
|
|
||||||
if (responsePdu[j] === 0x30) {
|
|
||||||
varbindsPos = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (varbindsPos !== -1) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (varbindsPos === -1) {
|
|
||||||
if (debug) {
|
|
||||||
console.log('Could not find varbinds in SNMPv3 response');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the varbinds
|
|
||||||
const varbindsLength = responsePdu[varbindsPos + 1];
|
|
||||||
const varbinds = responsePdu.slice(varbindsPos, varbindsPos + 2 + varbindsLength);
|
|
||||||
|
|
||||||
// Now search for values inside the varbinds
|
|
||||||
for (let i = 2; i < varbinds.length - 3; i++) {
|
|
||||||
// Look for a varbind sequence
|
|
||||||
if (varbinds[i] === 0x30) {
|
|
||||||
const varbindLength = varbinds[i + 1];
|
|
||||||
const varbind = varbinds.slice(i, i + 2 + varbindLength);
|
|
||||||
|
|
||||||
// Inside the varbind, look for the OID and then the value
|
|
||||||
for (let j = 0; j < varbind.length - 3; j++) {
|
|
||||||
if (varbind[j] === 0x06) { // OID
|
|
||||||
const oidLength = varbind[j + 1];
|
|
||||||
|
|
||||||
// The value should be right after the OID
|
|
||||||
const valuePos = j + 2 + oidLength;
|
|
||||||
if (valuePos < varbind.length - 1) {
|
|
||||||
// Check what type of value it is
|
|
||||||
if (varbind[valuePos] === 0x02) { // INTEGER
|
|
||||||
const valueLength = varbind[valuePos + 1];
|
|
||||||
const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found INTEGER value in SNMPv3 response:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} else if (varbind[valuePos] === 0x42) { // Gauge32
|
|
||||||
const valueLength = varbind[valuePos + 1];
|
|
||||||
const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found Gauge32 value in SNMPv3 response:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} else if (varbind[valuePos] === 0x43) { // TimeTicks
|
|
||||||
const valueLength = varbind[valuePos + 1];
|
|
||||||
const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength);
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found TimeTicks value in SNMPv3 response:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
} else if (varbind[valuePos] === 0x04) { // OctetString
|
|
||||||
const valueLength = varbind[valuePos + 1];
|
|
||||||
const value = varbind.slice(valuePos + 2, valuePos + 2 + valueLength).toString();
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found OctetString value in SNMPv3 response:', value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log('No valid value found in SNMPv3 response');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract engine ID from SNMPv3 response
|
|
||||||
* @param buffer Buffer containing the SNMP response
|
|
||||||
* @param debug Whether to enable debug output
|
|
||||||
* @returns Extracted engine ID or null if extraction failed
|
|
||||||
*/
|
|
||||||
public static extractEngineId(buffer: Buffer, debug: boolean = false): Buffer | null {
|
|
||||||
try {
|
|
||||||
// Simple parsing to find the engine ID
|
|
||||||
// Look for the first octet string with appropriate length
|
|
||||||
for (let i = 0; i < buffer.length - 10; i++) {
|
|
||||||
if (buffer[i] === 0x04) { // Octet string
|
|
||||||
const len = buffer[i + 1];
|
|
||||||
if (len >= 5 && len <= 32) { // Engine IDs are typically 5-32 bytes
|
|
||||||
// Verify this looks like an engine ID (usually starts with 0x80)
|
|
||||||
if (buffer[i + 2] === 0x80) {
|
|
||||||
if (debug) {
|
|
||||||
console.log('Found engine ID at position', i);
|
|
||||||
console.log('Engine ID:', buffer.slice(i + 2, i + 2 + len).toString('hex'));
|
|
||||||
}
|
|
||||||
return buffer.slice(i + 2, i + 2 + len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error extracting engine ID:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,10 +2,12 @@
|
|||||||
* Type definitions for SNMP module
|
* Type definitions for SNMP module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Buffer } from 'node:buffer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UPS status interface
|
* UPS status interface
|
||||||
*/
|
*/
|
||||||
export interface UpsStatus {
|
export interface IUpsStatus {
|
||||||
/** Current power status */
|
/** Current power status */
|
||||||
powerStatus: 'online' | 'onBattery' | 'unknown';
|
powerStatus: 'online' | 'onBattery' | 'unknown';
|
||||||
/** Battery capacity percentage */
|
/** Battery capacity percentage */
|
||||||
@@ -19,24 +21,31 @@ export interface UpsStatus {
|
|||||||
/**
|
/**
|
||||||
* SNMP OID Sets for different UPS brands
|
* SNMP OID Sets for different UPS brands
|
||||||
*/
|
*/
|
||||||
export interface OIDSet {
|
export interface IOidSet {
|
||||||
/** OID for power status */
|
/** OID for power status */
|
||||||
POWER_STATUS: string;
|
POWER_STATUS: string;
|
||||||
/** OID for battery capacity */
|
/** OID for battery capacity */
|
||||||
BATTERY_CAPACITY: string;
|
BATTERY_CAPACITY: string;
|
||||||
/** OID for battery runtime */
|
/** OID for battery runtime */
|
||||||
BATTERY_RUNTIME: string;
|
BATTERY_RUNTIME: string;
|
||||||
|
/** Power status value mappings */
|
||||||
|
POWER_STATUS_VALUES?: {
|
||||||
|
/** SNMP value that indicates UPS is online (on AC power) */
|
||||||
|
online: number;
|
||||||
|
/** SNMP value that indicates UPS is on battery */
|
||||||
|
onBattery: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supported UPS model types
|
* Supported UPS model types
|
||||||
*/
|
*/
|
||||||
export type UpsModel = 'cyberpower' | 'apc' | 'eaton' | 'tripplite' | 'liebert' | 'custom';
|
export type TUpsModel = 'cyberpower' | 'apc' | 'eaton' | 'tripplite' | 'liebert' | 'custom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNMP Configuration interface
|
* SNMP Configuration interface
|
||||||
*/
|
*/
|
||||||
export interface SnmpConfig {
|
export interface ISnmpConfig {
|
||||||
/** SNMP server host */
|
/** SNMP server host */
|
||||||
host: string;
|
host: string;
|
||||||
/** SNMP server port (default 161) */
|
/** SNMP server port (default 161) */
|
||||||
@@ -45,11 +54,13 @@ export interface SnmpConfig {
|
|||||||
version: number;
|
version: number;
|
||||||
/** Timeout in milliseconds */
|
/** Timeout in milliseconds */
|
||||||
timeout: number;
|
timeout: number;
|
||||||
|
|
||||||
|
context?: string;
|
||||||
|
|
||||||
// SNMPv1/v2c
|
// SNMPv1/v2c
|
||||||
/** Community string for SNMPv1/v2c */
|
/** Community string for SNMPv1/v2c */
|
||||||
community?: string;
|
community?: string;
|
||||||
|
|
||||||
// SNMPv3
|
// SNMPv3
|
||||||
/** Security level for SNMPv3 */
|
/** Security level for SNMPv3 */
|
||||||
securityLevel?: 'noAuthNoPriv' | 'authNoPriv' | 'authPriv';
|
securityLevel?: 'noAuthNoPriv' | 'authNoPriv' | 'authPriv';
|
||||||
@@ -63,18 +74,18 @@ export interface SnmpConfig {
|
|||||||
privProtocol?: 'DES' | 'AES';
|
privProtocol?: 'DES' | 'AES';
|
||||||
/** Privacy key for SNMPv3 */
|
/** Privacy key for SNMPv3 */
|
||||||
privKey?: string;
|
privKey?: string;
|
||||||
|
|
||||||
// UPS model and custom OIDs
|
// UPS model and custom OIDs
|
||||||
/** UPS model for OID selection */
|
/** UPS model for OID selection */
|
||||||
upsModel?: UpsModel;
|
upsModel?: TUpsModel;
|
||||||
/** Custom OIDs when using custom UPS model */
|
/** Custom OIDs when using custom UPS model */
|
||||||
customOIDs?: OIDSet;
|
customOIDs?: IOidSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNMPv3 security parameters
|
* SNMPv3 security parameters
|
||||||
*/
|
*/
|
||||||
export interface SnmpV3SecurityParams {
|
export interface ISnmpV3SecurityParams {
|
||||||
/** Engine ID for the SNMP server */
|
/** Engine ID for the SNMP server */
|
||||||
msgAuthoritativeEngineID: Buffer;
|
msgAuthoritativeEngineID: Buffer;
|
||||||
/** Engine boots counter */
|
/** Engine boots counter */
|
||||||
@@ -87,4 +98,4 @@ export interface SnmpV3SecurityParams {
|
|||||||
msgAuthenticationParameters: Buffer;
|
msgAuthenticationParameters: Buffer;
|
||||||
/** Privacy parameters */
|
/** Privacy parameters */
|
||||||
msgPrivacyParameters: Buffer;
|
msgPrivacyParameters: Buffer;
|
||||||
}
|
}
|
||||||
|
308
ts/systemd.ts
308
ts/systemd.ts
@@ -1,6 +1,9 @@
|
|||||||
import { promises as fs } from 'fs';
|
import process from 'node:process';
|
||||||
import { execSync } from 'child_process';
|
import { promises as fs } from 'node:fs';
|
||||||
import { NupstDaemon } from './daemon.js';
|
import { execSync } from 'node:child_process';
|
||||||
|
import { NupstDaemon } from './daemon.ts';
|
||||||
|
import { logger } from './logger.ts';
|
||||||
|
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for managing systemd service
|
* Class for managing systemd service
|
||||||
@@ -13,17 +16,17 @@ export class NupstSystemd {
|
|||||||
|
|
||||||
/** Template for the systemd service file */
|
/** Template for the systemd service file */
|
||||||
private readonly serviceTemplate = `[Unit]
|
private readonly serviceTemplate = `[Unit]
|
||||||
Description=Node.js UPS Shutdown Tool
|
Description=NUPST - Deno-powered UPS Monitoring Tool
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/nupst daemon-start
|
ExecStart=/usr/local/bin/nupst service start-daemon
|
||||||
Restart=always
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
User=root
|
User=root
|
||||||
Group=root
|
Group=root
|
||||||
Environment=PATH=/usr/bin:/usr/local/bin
|
Environment=PATH=/usr/bin:/usr/local/bin
|
||||||
Environment=NODE_ENV=production
|
WorkingDirectory=/opt/nupst
|
||||||
WorkingDirectory=/tmp
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -47,10 +50,11 @@ WantedBy=multi-user.target
|
|||||||
try {
|
try {
|
||||||
await fs.access(configPath);
|
await fs.access(configPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('┌─ Configuration Error ─────────────────────┐');
|
logger.log('');
|
||||||
console.error(`│ No configuration file found at ${configPath}`);
|
logger.error('No configuration found');
|
||||||
console.error('│ Please run \'nupst setup\' first to create a configuration.');
|
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
|
||||||
console.error('└──────────────────────────────────────────┘');
|
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
|
||||||
|
logger.log('');
|
||||||
throw new Error('Configuration not found');
|
throw new Error('Configuration not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,26 +67,27 @@ WantedBy=multi-user.target
|
|||||||
try {
|
try {
|
||||||
// Check if configuration exists before installing
|
// Check if configuration exists before installing
|
||||||
await this.checkConfigExists();
|
await this.checkConfigExists();
|
||||||
|
|
||||||
// Write the service file
|
// Write the service file
|
||||||
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
|
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
|
||||||
console.log('┌─ Service Installation ─────────────────────┐');
|
const boxWidth = 50;
|
||||||
console.log(`│ Service file created at ${this.serviceFilePath}`);
|
logger.logBoxTitle('Service Installation', boxWidth);
|
||||||
|
logger.logBoxLine(`Service file created at ${this.serviceFilePath}`);
|
||||||
|
|
||||||
// Reload systemd daemon
|
// Reload systemd daemon
|
||||||
execSync('systemctl daemon-reload');
|
execSync('systemctl daemon-reload');
|
||||||
console.log('│ Systemd daemon reloaded');
|
logger.logBoxLine('Systemd daemon reloaded');
|
||||||
|
|
||||||
// Enable the service
|
// Enable the service
|
||||||
execSync('systemctl enable nupst.service');
|
execSync('systemctl enable nupst.service');
|
||||||
console.log('│ Service enabled to start on boot');
|
logger.logBoxLine('Service enabled to start on boot');
|
||||||
console.log('└──────────────────────────────────────────┘');
|
logger.logBoxEnd();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message === 'Configuration not found') {
|
if (error instanceof Error && error.message === 'Configuration not found') {
|
||||||
// Just rethrow the error as the message has already been displayed
|
// Just rethrow the error as the message has already been displayed
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
console.error('Failed to install systemd service:', error);
|
logger.error(`Failed to install systemd service: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,17 +100,18 @@ WantedBy=multi-user.target
|
|||||||
try {
|
try {
|
||||||
// Check if configuration exists before starting
|
// Check if configuration exists before starting
|
||||||
await this.checkConfigExists();
|
await this.checkConfigExists();
|
||||||
|
|
||||||
execSync('systemctl start nupst.service');
|
execSync('systemctl start nupst.service');
|
||||||
console.log('┌─ Service Status ─────────────────────────┐');
|
const boxWidth = 45;
|
||||||
console.log('│ NUPST service started successfully');
|
logger.logBoxTitle('Service Status', boxWidth);
|
||||||
console.log('└──────────────────────────────────────────┘');
|
logger.logBoxLine('NUPST service started successfully');
|
||||||
|
logger.logBoxEnd();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message === 'Configuration not found') {
|
if (error instanceof Error && error.message === 'Configuration not found') {
|
||||||
// Exit with error code since configuration is required
|
// Exit with error code since configuration is required
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.error('Failed to start service:', error);
|
logger.error(`Failed to start service: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,33 +120,86 @@ WantedBy=multi-user.target
|
|||||||
* Stop the systemd service
|
* Stop the systemd service
|
||||||
* @throws Error if stop fails
|
* @throws Error if stop fails
|
||||||
*/
|
*/
|
||||||
public async stop(): Promise<void> {
|
public stop(): void {
|
||||||
try {
|
try {
|
||||||
execSync('systemctl stop nupst.service');
|
execSync('systemctl stop nupst.service');
|
||||||
console.log('NUPST service stopped');
|
logger.success('NUPST service stopped');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to stop service:', error);
|
logger.error(`Failed to stop service: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get status of the systemd service and UPS
|
* Get status of the systemd service and UPS
|
||||||
|
* @param debugMode Whether to enable debug mode for SNMP
|
||||||
*/
|
*/
|
||||||
public async getStatus(): Promise<void> {
|
/**
|
||||||
|
* Display version information and update status
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async displayVersionInfo(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Check if config exists first
|
const nupst = this.daemon.getNupstSnmp().getNupst();
|
||||||
|
const version = nupst.getVersion();
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
const updateAvailable = await nupst.checkForUpdates();
|
||||||
|
|
||||||
|
// Display version info
|
||||||
|
if (updateAvailable) {
|
||||||
|
const updateStatus = nupst.getUpdateStatus();
|
||||||
|
logger.log('');
|
||||||
|
logger.log(
|
||||||
|
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)}`,
|
||||||
|
);
|
||||||
|
logger.log(` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`);
|
||||||
|
} else {
|
||||||
|
logger.log('');
|
||||||
|
logger.log(
|
||||||
|
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${theme.success('Up to date')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If version check fails, show at least the current version
|
||||||
|
try {
|
||||||
|
const nupst = this.daemon.getNupstSnmp().getNupst();
|
||||||
|
const version = nupst.getVersion();
|
||||||
|
logger.log('');
|
||||||
|
logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`);
|
||||||
|
} catch (_innerError) {
|
||||||
|
// Silently fail if we can't even get the version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getStatus(debugMode: boolean = false): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Enable debug mode if requested
|
||||||
|
if (debugMode) {
|
||||||
|
console.log('');
|
||||||
|
logger.info('Debug Mode: SNMP debugging enabled');
|
||||||
|
console.log('');
|
||||||
|
this.daemon.getNupstSnmp().enableDebug();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display version and update status first
|
||||||
|
await this.displayVersionInfo();
|
||||||
|
|
||||||
|
// Check if config exists
|
||||||
try {
|
try {
|
||||||
await this.checkConfigExists();
|
await this.checkConfigExists();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error message already displayed by checkConfigExists
|
// Error message already displayed by checkConfigExists
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.displayServiceStatus();
|
await this.displayServiceStatus();
|
||||||
await this.displayUpsStatus();
|
await this.displayAllUpsStatus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get status: ${error.message}`);
|
logger.error(
|
||||||
|
`Failed to get status: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,38 +207,153 @@ WantedBy=multi-user.target
|
|||||||
* Display the systemd service status
|
* Display the systemd service status
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async displayServiceStatus(): Promise<void> {
|
private displayServiceStatus(): void {
|
||||||
try {
|
try {
|
||||||
const serviceStatus = execSync('systemctl status nupst.service').toString();
|
const serviceStatus = execSync('systemctl status nupst.service').toString();
|
||||||
console.log('┌─ Service Status ─────────────────────────┐');
|
const lines = serviceStatus.split('\n');
|
||||||
console.log(serviceStatus.split('\n').map(line => `│ ${line}`).join('\n'));
|
|
||||||
console.log('└──────────────────────────────────────────┘');
|
// Parse key information from systemctl output
|
||||||
|
let isActive = false;
|
||||||
|
let pid = '';
|
||||||
|
let memory = '';
|
||||||
|
let cpu = '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('Active:')) {
|
||||||
|
isActive = line.includes('active (running)');
|
||||||
|
} else if (line.includes('Main PID:')) {
|
||||||
|
const match = line.match(/Main PID:\s+(\d+)/);
|
||||||
|
if (match) pid = match[1];
|
||||||
|
} else if (line.includes('Memory:')) {
|
||||||
|
const match = line.match(/Memory:\s+([\d.]+[A-Z])/);
|
||||||
|
if (match) memory = match[1];
|
||||||
|
} else if (line.includes('CPU:')) {
|
||||||
|
const match = line.match(/CPU:\s+([\d.]+(?:ms|s))/);
|
||||||
|
if (match) cpu = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display beautiful status
|
||||||
|
logger.log('');
|
||||||
|
if (isActive) {
|
||||||
|
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
|
||||||
|
} else {
|
||||||
|
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid || memory || cpu) {
|
||||||
|
const details = [];
|
||||||
|
if (pid) details.push(`PID: ${theme.dim(pid)}`);
|
||||||
|
if (memory) details.push(`Memory: ${theme.dim(memory)}`);
|
||||||
|
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
|
||||||
|
logger.log(` ${details.join(' ')}`);
|
||||||
|
}
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('┌─ Service Status ─────────────────────────┐');
|
logger.log('');
|
||||||
console.error('│ Service is not running');
|
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
|
||||||
console.error('└──────────────────────────────────────────┘');
|
logger.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the UPS status
|
* Display all UPS statuses
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async displayUpsStatus(): Promise<void> {
|
private async displayAllUpsStatus(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const upsStatus = await this.daemon.getConfig().snmp;
|
// Explicitly load the configuration first to ensure it's up-to-date
|
||||||
|
await this.daemon.loadConfig();
|
||||||
|
const config = this.daemon.getConfig();
|
||||||
const snmp = this.daemon.getNupstSnmp();
|
const snmp = this.daemon.getNupstSnmp();
|
||||||
const status = await snmp.getUpsStatus(upsStatus);
|
|
||||||
|
// Check if we have the new multi-UPS config format
|
||||||
console.log('┌─ UPS Status ───────────────────────────────┐');
|
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||||
console.log(`│ Power Status: ${status.powerStatus}`);
|
logger.info(`UPS Devices (${config.upsDevices.length}):`);
|
||||||
console.log(`│ Battery Capacity: ${status.batteryCapacity}%`);
|
|
||||||
console.log(`│ Runtime Remaining: ${status.batteryRuntime} minutes`);
|
// Show status for each UPS
|
||||||
console.log('└──────────────────────────────────────────┘');
|
for (const ups of config.upsDevices) {
|
||||||
|
await this.displaySingleUpsStatus(ups, snmp);
|
||||||
|
}
|
||||||
|
} else if (config.snmp) {
|
||||||
|
// Legacy single UPS configuration
|
||||||
|
logger.info('UPS Devices (1):');
|
||||||
|
const legacyUps = {
|
||||||
|
id: 'default',
|
||||||
|
name: 'Default UPS',
|
||||||
|
snmp: config.snmp,
|
||||||
|
thresholds: config.thresholds,
|
||||||
|
groups: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||||
|
} else {
|
||||||
|
logger.log('');
|
||||||
|
logger.warn('No UPS devices configured');
|
||||||
|
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
|
||||||
|
logger.log('');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('┌─ UPS Status ───────────────────────────────┐');
|
logger.log('');
|
||||||
console.error(`│ Failed to retrieve UPS status: ${error.message}`);
|
logger.error('Failed to retrieve UPS status');
|
||||||
console.error('└──────────────────────────────────────────┘');
|
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||||
|
logger.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display status of a single UPS
|
||||||
|
* @param ups UPS configuration
|
||||||
|
* @param snmp SNMP manager
|
||||||
|
*/
|
||||||
|
private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Create a test config with a short timeout
|
||||||
|
const testConfig = {
|
||||||
|
...ups.snmp,
|
||||||
|
timeout: Math.min(ups.snmp.timeout, 10000), // Use at most 10 seconds for status check
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = await snmp.getUpsStatus(testConfig);
|
||||||
|
|
||||||
|
// Determine status symbol based on power status
|
||||||
|
let statusSymbol = symbols.unknown;
|
||||||
|
if (status.powerStatus === 'online') {
|
||||||
|
statusSymbol = symbols.running;
|
||||||
|
} else if (status.powerStatus === 'onBattery') {
|
||||||
|
statusSymbol = symbols.warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display UPS name and power status
|
||||||
|
logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
|
||||||
|
|
||||||
|
// Display battery with color coding
|
||||||
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||||
|
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
|
||||||
|
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
||||||
|
|
||||||
|
// Display host info
|
||||||
|
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||||
|
|
||||||
|
// Display groups if any
|
||||||
|
if (ups.groups && ups.groups.length > 0) {
|
||||||
|
const config = this.daemon.getConfig();
|
||||||
|
const groupNames = ups.groups.map((groupId: string) => {
|
||||||
|
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
|
||||||
|
return group ? group.name : groupId;
|
||||||
|
});
|
||||||
|
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Display error for this UPS
|
||||||
|
logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
|
||||||
|
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||||
|
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||||
|
logger.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,13 +366,13 @@ WantedBy=multi-user.target
|
|||||||
await this.stopService();
|
await this.stopService();
|
||||||
await this.disableService();
|
await this.disableService();
|
||||||
await this.removeServiceFile();
|
await this.removeServiceFile();
|
||||||
|
|
||||||
// Reload systemd daemon
|
// Reload systemd daemon
|
||||||
execSync('systemctl daemon-reload');
|
execSync('systemctl daemon-reload');
|
||||||
console.log('Systemd daemon reloaded');
|
logger.log('Systemd daemon reloaded');
|
||||||
console.log('NUPST service has been successfully uninstalled');
|
logger.success('NUPST service has been successfully uninstalled');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to disable and uninstall service:', error);
|
logger.error(`Failed to disable and uninstall service: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,13 +381,13 @@ WantedBy=multi-user.target
|
|||||||
* Stop the service if it's running
|
* Stop the service if it's running
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async stopService(): Promise<void> {
|
private stopService(): void {
|
||||||
try {
|
try {
|
||||||
console.log('Stopping NUPST service...');
|
logger.log('Stopping NUPST service...');
|
||||||
execSync('systemctl stop nupst.service');
|
execSync('systemctl stop nupst.service');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Service might not be running, that's okay
|
// 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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,12 +395,12 @@ WantedBy=multi-user.target
|
|||||||
* Disable the service
|
* Disable the service
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async disableService(): Promise<void> {
|
private disableService(): void {
|
||||||
try {
|
try {
|
||||||
console.log('Disabling NUPST service...');
|
logger.log('Disabling NUPST service...');
|
||||||
execSync('systemctl disable nupst.service');
|
execSync('systemctl disable nupst.service');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Service was not enabled or could not be disabled');
|
logger.log('Service was not enabled or could not be disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,11 +410,11 @@ WantedBy=multi-user.target
|
|||||||
*/
|
*/
|
||||||
private async removeServiceFile(): Promise<void> {
|
private async removeServiceFile(): Promise<void> {
|
||||||
if (await fs.stat(this.serviceFilePath).catch(() => null)) {
|
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);
|
await fs.unlink(this.serviceFilePath);
|
||||||
console.log('Service file removed');
|
logger.log('Service file removed');
|
||||||
} else {
|
} else {
|
||||||
console.log('Service file did not exist');
|
logger.log('Service file did not exist');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"verbatimModuleSyntax": true
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"dist_*/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
65
uninstall.sh
65
uninstall.sh
@@ -5,13 +5,22 @@
|
|||||||
|
|
||||||
# Check if running as root
|
# Check if running as root
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
echo "Please run as root (sudo ./uninstall.sh)"
|
echo "Please run as root (sudo nupst uninstall or sudo ./uninstall.sh)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# This script can be called directly or through the CLI
|
||||||
|
# When called through the CLI, environment variables are set
|
||||||
|
# REMOVE_CONFIG=yes|no - whether to remove configuration files
|
||||||
|
# REMOVE_REPO=yes|no - whether to remove the repository
|
||||||
|
|
||||||
|
# If not set through CLI, use defaults
|
||||||
|
REMOVE_CONFIG=${REMOVE_CONFIG:-"no"}
|
||||||
|
REMOVE_REPO=${REMOVE_REPO:-"no"}
|
||||||
|
|
||||||
echo "NUPST Uninstaller"
|
echo "NUPST Uninstaller"
|
||||||
echo "================="
|
echo "================="
|
||||||
echo "This script will completely remove NUPST from your system."
|
echo "This will completely remove NUPST from your system."
|
||||||
|
|
||||||
# Find the directory where this script is located
|
# Find the directory where this script is located
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
@@ -37,20 +46,52 @@ if [ -L "/usr/local/bin/nupst" ]; then
|
|||||||
rm -f /usr/local/bin/nupst
|
rm -f /usr/local/bin/nupst
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 3: Ask about removing configuration
|
# Step 3: Remove configuration if requested
|
||||||
read -p "Do you want to remove the NUPST configuration files? (y/N) " -n 1 -r
|
if [ "$REMOVE_CONFIG" = "yes" ]; then
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
echo "Removing configuration files..."
|
echo "Removing configuration files..."
|
||||||
rm -rf /etc/nupst
|
rm -rf /etc/nupst
|
||||||
|
else
|
||||||
|
# If not called through CLI, ask user
|
||||||
|
if [ -z "$NUPST_CLI_CALL" ]; then
|
||||||
|
read -p "Do you want to remove the NUPST configuration files? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Removing configuration files..."
|
||||||
|
rm -rf /etc/nupst
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 4: Check if this was a git installation
|
# Step 4: Remove repository if requested
|
||||||
if [ -d "$SCRIPT_DIR/.git" ]; then
|
if [ "$REMOVE_REPO" = "yes" ]; then
|
||||||
echo
|
if [ -d "$SCRIPT_DIR/.git" ]; then
|
||||||
echo "This appears to be a git installation. The local repository will remain intact."
|
echo "Removing NUPST repository directory..."
|
||||||
echo "If you wish to completely remove it, you can delete the directory:"
|
|
||||||
echo " rm -rf $SCRIPT_DIR"
|
# Get parent directory to remove it after the script exits
|
||||||
|
PARENT_DIR=$(dirname "$SCRIPT_DIR")
|
||||||
|
REPO_NAME=$(basename "$SCRIPT_DIR")
|
||||||
|
|
||||||
|
# Create a temporary cleanup script
|
||||||
|
CLEANUP_SCRIPT=$(mktemp)
|
||||||
|
echo "#!/bin/bash" > "$CLEANUP_SCRIPT"
|
||||||
|
echo "sleep 1" >> "$CLEANUP_SCRIPT"
|
||||||
|
echo "rm -rf \"$SCRIPT_DIR\"" >> "$CLEANUP_SCRIPT"
|
||||||
|
echo "echo \"NUPST repository has been removed.\"" >> "$CLEANUP_SCRIPT"
|
||||||
|
chmod +x "$CLEANUP_SCRIPT"
|
||||||
|
|
||||||
|
# Run the cleanup script in the background
|
||||||
|
nohup "$CLEANUP_SCRIPT" > /dev/null 2>&1 &
|
||||||
|
|
||||||
|
echo "NUPST repository will be removed after uninstaller exits."
|
||||||
|
else
|
||||||
|
echo "No git repository found."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# If not requested, just display info
|
||||||
|
if [ -d "$SCRIPT_DIR/.git" ]; then
|
||||||
|
echo
|
||||||
|
echo "NUPST repository at $SCRIPT_DIR will remain intact."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for npm global installation
|
# Check for npm global installation
|
||||||
|
Reference in New Issue
Block a user