diff --git a/deno.json b/deno.json index 4ee8f16..f792084 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@serve.zone/nupst", - "version": "4.0.3", + "version": "4.0.4", "exports": "./mod.ts", "tasks": { "dev": "deno run --allow-all mod.ts", diff --git a/install.sh b/install.sh index a84f30b..f60b790 100644 --- a/install.sh +++ b/install.sh @@ -98,7 +98,11 @@ if [ ! -t 0 ] || [ ! -t 1 ]; then echo "Script detected it's running in a non-interactive environment without -y flag." echo "Attempting to find a controlling terminal for interactive prompts..." # Try to use a controlling terminal for user input - exec < /dev/tty 2>/dev/null || INTERACTIVE=0 + if exec < /dev/tty 2>/dev/null && [ -t 0 ]; then + INTERACTIVE=1 + else + INTERACTIVE=0 + fi if [ $INTERACTIVE -eq 0 ]; then echo "ERROR: No controlling terminal available for interactive prompts." diff --git a/test/manualdocker/00-test-fresh-v4-install.sh b/test/manualdocker/00-test-fresh-v4-install.sh new file mode 100755 index 0000000..517caff --- /dev/null +++ b/test/manualdocker/00-test-fresh-v4-install.sh @@ -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 "" diff --git a/ts/migrations/migration-v3-to-v4.ts b/ts/migrations/migration-v3-to-v4.ts index 5e59412..30b3ed5 100644 --- a/ts/migrations/migration-v3-to-v4.ts +++ b/ts/migrations/migration-v3-to-v4.ts @@ -4,19 +4,38 @@ import { logger } from '../logger.ts'; /** * Migration from v3 (upsList) to v4 (upsDevices) * - * Detects v3 format: + * Transforms v3 format with flat SNMP config: * { - * upsList: [ ... ], - * groups: [ ... ], - * checkInterval: 30000 + * upsList: [ + * { + * id: "ups-1", + * name: "UPS 1", + * host: "192.168.1.1", + * port: 161, + * community: "public", + * version: "1" // string + * } + * ] * } * - * Converts to: + * To v4 format with nested SNMP config: * { * version: "4.0", - * upsDevices: [ ... ], // renamed from upsList - * groups: [ ... ], - * checkInterval: 30000 + * 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 { @@ -30,16 +49,58 @@ export class MigrationV3ToV4 extends BaseMigration { } async migrate(config: any): Promise { - logger.info(`${this.getName()}: Renaming upsList to upsDevices...`); + logger.info(`${this.getName()}: Migrating v3 config to v4 format...`); + logger.dim(` - Renaming upsList → upsDevices`); + logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`); + + // Transform each UPS device from v3 flat structure to v4 nested structure + const transformedDevices = config.upsList.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: config.upsList, // Rename upsList → upsDevices + upsDevices: transformedDevices, groups: config.groups || [], checkInterval: config.checkInterval || 30000, }; - logger.success(`${this.getName()}: Migration complete`); + logger.success(`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`); return migrated; } }