- 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>
298 lines
9.9 KiB
TypeScript
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,
|
|
}
|
|
}
|
|
}
|