Files
adam-acs/app/services/migration_service.ts
T
Kevin L'HereecandCopilot d1c65dbf1e chore: initial commit — AdonisJS v7 TR-069 ACS
- Docker infrastructure (Dockerfile, docker-compose.yml, genieacs-sim)
- AdonisJS v7 app with Inertia.js + Vue 3
- MongoDB integration
- CWMP/TR-069 state machine engine
- Configuration engine with compliance tracking
- Branding: ADAM ACS (favicon, navbar, locales)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 16:46:37 +02:00

298 lines
9.9 KiB
TypeScript

import ConfigurationService from './configuration_service.js'
import FirmwareCatalogService from './firmware_catalog_service.js'
import type { CreateConfigurationDto } from './configuration_service.js'
import type { CreateFirmwareEntryDto } from './firmware_catalog_service.js'
import { Db } from 'mongodb'
interface MigrationReport {
discoveryMigrated: number
discoverySkipped: number
provisioningMigrated: number
provisioningSkipped: number
firmwareEntriesCreated: number
errors: string[]
}
interface RollbackReport {
discoveryRolledBack: number
provisioningRolledBack: number
errors: string[]
}
export default class MigrationService {
constructor(
private db: Db,
private configService = new ConfigurationService(),
private firmwareCatalogService = new FirmwareCatalogService(),
) {}
/**
* Migrate all legacy Discovery and Provisioning configs to unified Configuration format
*/
async migrateAll(): Promise<MigrationReport> {
const report: MigrationReport = {
discoveryMigrated: 0,
discoverySkipped: 0,
provisioningMigrated: 0,
provisioningSkipped: 0,
firmwareEntriesCreated: 0,
errors: [],
}
try {
// Migrate Discovery configs (inventory + maintain policy)
const discoveryCollection = this.db.collection('discovery_configs')
const discoveryConfigs = await discoveryCollection.find({}).toArray()
for (const legacy of discoveryConfigs) {
try {
const migrated = await this.migrateDiscoveryConfig(legacy)
if (migrated) {
report.discoveryMigrated++
} else {
report.discoverySkipped++
}
} catch (error) {
report.discoverySkipped++
report.errors.push(`Discovery ${legacy.identifier}: ${(error as Error).message}`)
}
}
// Migrate Provisioning configs (desired parameters + once/maintain + firmware)
const provisioningCollection = this.db.collection('provisioning_configs')
const provisioningConfigs = await provisioningCollection.find({}).toArray()
for (const legacy of provisioningConfigs) {
try {
const migrated = await this.migrateProvisioningConfig(legacy)
if (migrated) {
report.provisioningMigrated++
} else {
report.provisioningSkipped++
}
// Extract firmware Downloads into FirmwareCatalog
if (legacy.downloadActions && legacy.downloadActions.length > 0) {
for (const action of legacy.downloadActions) {
try {
await this.extractFirmwareEntry(action, legacy)
report.firmwareEntriesCreated++
} catch (error) {
report.errors.push(`Firmware extraction for ${legacy.identifier}: ${(error as Error).message}`)
}
}
}
} catch (error) {
report.provisioningSkipped++
report.errors.push(`Provisioning ${legacy.identifier}: ${(error as Error).message}`)
}
}
} catch (error) {
report.errors.push(`Migration error: ${(error as Error).message}`)
}
return report
}
/**
* Migrate a single Discovery config to Configuration
* Discovery = inventory paths + "maintain" policy
*/
private async migrateDiscoveryConfig(legacy: any): Promise<boolean> {
const criteria = this.buildCriteriaFromLegacy(legacy)
const dto: CreateConfigurationDto = {
identifier: `legacy-disc-${legacy.identifier}`,
name: `[Migrated] ${legacy.name || legacy.identifier}`,
version: '1.0.0',
priority: 100,
policy: 'maintain',
enabled: true,
tags: ['legacy', 'discovery'],
criteria,
inventory: (legacy.modelPaths || []).map((p: string) => ({ path: p })),
desiredParameters: [],
}
// Check if already exists (skip if yes)
const existing = await this.configService.findById(dto.identifier)
if (existing) {
return false
}
await this.configService.create(dto)
return true
}
/**
* Migrate a single Provisioning config to Configuration
* Provisioning = desired parameters + (once OR maintain) policy + firmware migration
*/
private async migrateProvisioningConfig(legacy: any): Promise<boolean> {
const criteria = this.buildCriteriaFromLegacy(legacy)
const policy = legacy.downloadActions?.length > 0 ? 'once' : 'maintain'
const desiredParameters = this.buildDesiredParametersFromLegacy(legacy)
// Build inventory paths from desired parameters (auto-suggest)
const inventoryPaths = new Set<string>()
for (const param of desiredParameters) {
const parts = param.name.split('.')
parts.pop()
inventoryPaths.add(parts.join('.') + '.')
}
const dto: CreateConfigurationDto = {
identifier: `legacy-prov-${legacy.identifier}`,
name: `[Migrated] ${legacy.name || legacy.identifier}`,
version: '1.0.0',
priority: 100,
policy,
enabled: true,
tags: ['legacy', 'provisioning'],
criteria,
inventory: [...inventoryPaths].map((p) => ({ path: p })),
desiredParameters,
}
const existing = await this.configService.findById(dto.identifier)
if (existing) {
return false
}
await this.configService.create(dto)
return true
}
/**
* Build criteria object from legacy config device filters
*/
private buildCriteriaFromLegacy(legacy: any): Record<string, string> {
const criteria: Record<string, string> = {}
if (legacy.manufacturer) criteria.manufacturer = legacy.manufacturer
if (legacy.oui) criteria.oui = legacy.oui
if (legacy.productClass) criteria.productClass = legacy.productClass
if (legacy.serialNumber) criteria.serialNumber = legacy.serialNumber
return criteria
}
/**
* Build desired parameters from legacy Set Parameter actions
*/
private buildDesiredParametersFromLegacy(legacy: any): Array<{ name: string; value: string | number | boolean; type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' }> {
const params: Array<{ name: string; value: string | number | boolean; type: 'string' | 'int' | 'unsignedInt' | 'boolean' | 'dateTime' }> = []
if (!legacy.actions) return params
for (const action of legacy.actions) {
if (action.type === 'SET_PARAMETER' && action.parameter && action.value !== undefined) {
params.push({
name: action.parameter,
value: action.value,
type: typeof action.value === 'boolean' ? 'boolean' : typeof action.value === 'number' ? 'int' : 'string',
})
}
}
return params
}
/**
* Extract firmware Download action into FirmwareCatalog entry
*/
private async extractFirmwareEntry(action: any, legacy: any): Promise<void> {
const entry: CreateFirmwareEntryDto = {
identifier: `legacy-fw-${legacy.productClass || 'device'}-${action.version || '1.0.0'}`,
productClass: legacy.productClass || 'Device',
version: action.version || '1.0.0',
downloadUrl: action.url || action.firmwareUrl || '',
checksum: action.checksum || '',
checksumType: 'md5',
fileSize: action.fileSize || 0,
}
try {
await this.firmwareCatalogService.createEntry(entry)
} catch {
// Entry may already exist — skip silently
}
}
/**
* Rollback migration: reconstruct legacy collections from prefixed configurations
*/
async rollback(): Promise<RollbackReport> {
const report: RollbackReport = {
discoveryRolledBack: 0,
provisioningRolledBack: 0,
errors: [],
}
try {
const result = await this.configService.findAll({ perPage: 1000 })
const allConfigs = result.data
const discoveryCollection = this.db.collection('discovery_configs')
const provisioningCollection = this.db.collection('provisioning_configs')
for (const config of allConfigs) {
try {
if (config.identifier.startsWith('legacy-disc-')) {
const originalId = config.identifier.replace('legacy-disc-', '')
const legacy = this.buildLegacyDiscoveryFromConfig(config, originalId)
await discoveryCollection.insertOne(legacy)
report.discoveryRolledBack++
} else if (config.identifier.startsWith('legacy-prov-')) {
const originalId = config.identifier.replace('legacy-prov-', '')
const legacy = this.buildLegacyProvisioningFromConfig(config, originalId)
await provisioningCollection.insertOne(legacy)
report.provisioningRolledBack++
}
} catch (error) {
report.errors.push(`Rollback ${config.identifier}: ${(error as Error).message}`)
}
}
} catch (error) {
report.errors.push(`Rollback error: ${(error as Error).message}`)
}
return report
}
private buildLegacyDiscoveryFromConfig(config: any, originalId: string): any {
return {
identifier: originalId,
name: config.name.replace('[Migrated] ', ''),
manufacturer: config.criteria?.manufacturer,
oui: config.criteria?.oui,
productClass: config.criteria?.productClass,
serialNumber: config.criteria?.serialNumber,
modelPaths: (config.inventory || []).map((inv: any) => inv.path),
createdAt: config.createdAt,
updatedAt: config.updatedAt,
}
}
private buildLegacyProvisioningFromConfig(config: any, originalId: string): any {
const actions = config.desiredParameters?.map((param: any) => ({
type: 'SET_PARAMETER',
parameter: param.name,
value: param.value,
})) || []
return {
identifier: originalId,
name: config.name.replace('[Migrated] ', ''),
manufacturer: config.criteria?.manufacturer,
oui: config.criteria?.oui,
productClass: config.criteria?.productClass,
serialNumber: config.criteria?.serialNumber,
actions,
downloadActions: [],
createdAt: config.createdAt,
updatedAt: config.updatedAt,
}
}
}