The previous migration only checked for upsList field, but saveConfig() strips upsList when saving, creating a race condition. If the daemon restarted with a partially-migrated config (upsDevices with flat structure), the migration wouldn't run because it only looked for upsList. Now shouldRun() also detects: - upsDevices with flat structure (host at top level, no snmp object) And migrate() handles both: - config.upsList (pure v3) - config.upsDevices with flat structure (partially migrated) This fixes the "Cannot read properties of undefined (reading 'host')" error that occurred when configs had upsDevices but flat structure.
120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
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;
|
|
}
|
|
}
|