/** * Database seeder for cwmp_exchanges collection. * * Generates realistic CWMP exchange data for integration/manual testing. * Can be run standalone via: npx tsx database/seeders/cwmp_exchanges_seeder.ts * * Configuration via environment variables: * SEED_DEVICE_COUNT - Number of devices to generate (default: 5) * SEED_EXCHANGES_MIN - Minimum exchanges per device (default: 20) * SEED_EXCHANGES_MAX - Maximum exchanges per device (default: 100) * MONGODB_URI - MongoDB connection string * MONGODB_DB_NAME - Database name (default: adam-acs) */ import { MongoClient, type Db } from 'mongodb' // ─── Configuration ────────────────────────────────────────────────────────── interface SeederConfig { deviceCount: number exchangesMin: number exchangesMax: number mongoUri: string dbName: string } function getConfig(): SeederConfig { return { deviceCount: Number(process.env.SEED_DEVICE_COUNT ?? 5), exchangesMin: Number(process.env.SEED_EXCHANGES_MIN ?? 20), exchangesMax: Number(process.env.SEED_EXCHANGES_MAX ?? 100), mongoUri: process.env.MONGODB_URI ?? 'mongodb://localhost:27017', dbName: process.env.MONGODB_DB_NAME ?? 'adam-acs', } } // ─── CWMP Method Templates ────────────────────────────────────────────────── type Direction = 'CPE→ACS' | 'ACS→CPE' interface ExchangeTemplate { cwmpMethod: string direction: Direction xmlGenerator: () => string } const MANUFACTURERS = ['Sagemcom', 'Huawei', 'ZTE', 'Nokia', 'Technicolor'] const PRODUCT_CLASSES = ['Livebox 4', 'BM632w', 'ZXHN H298A', 'G-240W-A', 'TG789vac'] const OUI_LIST = ['F8B7FF', '202BC1', '001D08', '00E0FC', '74BD7B'] function randomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min } function randomChoice(arr: T[]): T { return arr[randomInt(0, arr.length - 1)] } function generateDeviceId(index: number): string { const oui = OUI_LIST[index % OUI_LIST.length] const serial = `DEVICE-${String(index + 1).padStart(3, '0')}` return `${oui}-${PRODUCT_CLASSES[index % PRODUCT_CLASSES.length]}-${serial}` } function generateSessionId(): string { const srcIp = `10.0.${randomInt(0, 255)}.${randomInt(1, 254)}` const srcPort = randomInt(30000, 65000) return `${srcIp}:${srcPort}->192.168.1.1:8080` } // ─── XML Body Generators ──────────────────────────────────────────────────── function wrapSoapEnvelope(body: string): string { return ` ${randomInt(100000, 999999)} ${body} ` } function generateInformXml(deviceIndex: number): string { const oui = OUI_LIST[deviceIndex % OUI_LIST.length] const manufacturer = MANUFACTURERS[deviceIndex % MANUFACTURERS.length] const productClass = PRODUCT_CLASSES[deviceIndex % PRODUCT_CLASSES.length] const serial = `DEVICE-${String(deviceIndex + 1).padStart(3, '0')}` const events = randomChoice([ ['0 BOOTSTRAP', '1 BOOT'], ['2 PERIODIC'], ['1 BOOT'], ['6 CONNECTION REQUEST'], ]) const eventString = events .map( (e, i) => `${e}${i}` ) .join('\n ') return wrapSoapEnvelope(` ${manufacturer} ${oui} ${productClass} ${serial} ${eventString} 1 ${new Date().toISOString()} 0 Device.DeviceInfo.HardwareVersion v2.1 Device.DeviceInfo.SoftwareVersion SR40_sip-fr-4.62.0 Device.ManagementServer.ConnectionRequestURL http://10.0.0.${randomInt(1, 254)}:7547/ Device.ManagementServer.ParameterKey `) } function generateInformResponseXml(): string { return wrapSoapEnvelope(` 1 `) } function generateGetParameterValuesXml(paths: string[]): string { const paramNames = paths.map((p) => `${p}`).join('\n ') return wrapSoapEnvelope(` ${paramNames} `) } function generateGetParameterValuesResponseXml(paths: string[]): string { const params = paths .map( (p) => ` ${p} value_${randomInt(1, 9999)} ` ) .join('\n ') return wrapSoapEnvelope(` ${params} `) } function generateSetParameterValuesXml(): string { return wrapSoapEnvelope(` Device.ManagementServer.PeriodicInformInterval ${randomInt(60, 3600)} Device.DeviceInfo.ProvisioningCode PROV-${randomInt(1000, 9999)} key-${randomInt(1, 999)} `) } function generateSetParameterValuesResponseXml(): string { return wrapSoapEnvelope(` 0 `) } function generateGetParameterNamesXml(): string { const path = randomChoice([ 'Device.', 'Device.DeviceInfo.', 'Device.ManagementServer.', 'Device.WiFi.', ]) return wrapSoapEnvelope(` ${path} 1 `) } function generateGetParameterNamesResponseXml(): string { const params = ['Device.DeviceInfo.', 'Device.ManagementServer.', 'Device.WiFi.', 'Device.IP.'] const paramList = params .map( (p) => ` ${p} 0 ` ) .join('\n ') return wrapSoapEnvelope(` ${paramList} `) } function generateDownloadXml(): string { return wrapSoapEnvelope(` firmware-update-${randomInt(1, 99)} 1 Firmware Upgrade Image http://firmware.example.com/fw-v${randomInt(1, 5)}.${randomInt(0, 9)}.bin admin **** ${randomInt(1000000, 50000000)} 0 `) } function generateDownloadResponseXml(): string { const status = randomChoice([0, 1]) // 0=completed, 1=in progress return wrapSoapEnvelope(` ${status} ${new Date().toISOString()} 0001-01-01T00:00:00Z `) } function generateRebootXml(): string { return wrapSoapEnvelope(` reboot-${randomInt(1, 999)} `) } function generateRebootResponseXml(): string { return wrapSoapEnvelope(``) } function generateFaultXml(): string { const faultCode = randomChoice(['9001', '9002', '9003', '9005', '9007']) const faultMessages: Record = { '9001': 'Request denied', '9002': 'Internal error', '9003': 'Invalid arguments', '9005': 'Invalid parameter name', '9007': 'Invalid parameter value', } return wrapSoapEnvelope(` Client CWMP fault ${faultCode} ${faultMessages[faultCode]} `) } function generateTransferCompleteXml(): string { return wrapSoapEnvelope(` firmware-update-${randomInt(1, 99)} 0 ${new Date().toISOString()} ${new Date().toISOString()} `) } function generateTransferCompleteResponseXml(): string { return wrapSoapEnvelope(``) } // ─── Exchange Sequence Patterns ───────────────────────────────────────────── /** Realistic CWMP session exchange patterns (request/response pairs) */ const SESSION_PATTERNS: ExchangeTemplate[][] = [ // Inform session start [ { cwmpMethod: 'Inform', direction: 'CPE→ACS', xmlGenerator: () => '' }, { cwmpMethod: 'InformResponse', direction: 'ACS→CPE', xmlGenerator: generateInformResponseXml }, ], // GetParameterValues [ { cwmpMethod: 'GetParameterValues', direction: 'ACS→CPE', xmlGenerator: () => generateGetParameterValuesXml([ 'Device.DeviceInfo.SoftwareVersion', 'Device.DeviceInfo.HardwareVersion', 'Device.ManagementServer.PeriodicInformInterval', ]), }, { cwmpMethod: 'GetParameterValuesResponse', direction: 'CPE→ACS', xmlGenerator: () => generateGetParameterValuesResponseXml([ 'Device.DeviceInfo.SoftwareVersion', 'Device.DeviceInfo.HardwareVersion', 'Device.ManagementServer.PeriodicInformInterval', ]), }, ], // SetParameterValues [ { cwmpMethod: 'SetParameterValues', direction: 'ACS→CPE', xmlGenerator: generateSetParameterValuesXml, }, { cwmpMethod: 'SetParameterValuesResponse', direction: 'CPE→ACS', xmlGenerator: generateSetParameterValuesResponseXml, }, ], // GetParameterNames [ { cwmpMethod: 'GetParameterNames', direction: 'ACS→CPE', xmlGenerator: generateGetParameterNamesXml, }, { cwmpMethod: 'GetParameterNamesResponse', direction: 'CPE→ACS', xmlGenerator: generateGetParameterNamesResponseXml, }, ], // Download (firmware) [ { cwmpMethod: 'Download', direction: 'ACS→CPE', xmlGenerator: generateDownloadXml }, { cwmpMethod: 'DownloadResponse', direction: 'CPE→ACS', xmlGenerator: generateDownloadResponseXml, }, ], // Reboot [ { cwmpMethod: 'Reboot', direction: 'ACS→CPE', xmlGenerator: generateRebootXml }, { cwmpMethod: 'RebootResponse', direction: 'CPE→ACS', xmlGenerator: generateRebootResponseXml }, ], // TransferComplete (CPE-initiated after download) [ { cwmpMethod: 'TransferComplete', direction: 'CPE→ACS', xmlGenerator: generateTransferCompleteXml, }, { cwmpMethod: 'TransferCompleteResponse', direction: 'ACS→CPE', xmlGenerator: generateTransferCompleteResponseXml, }, ], // Fault response [{ cwmpMethod: 'Fault', direction: 'CPE→ACS', xmlGenerator: generateFaultXml }], // Empty POST (session end) [{ cwmpMethod: 'Empty', direction: 'CPE→ACS', xmlGenerator: () => '' }], ] // ─── Exchange Generator ───────────────────────────────────────────────────── interface CwmpExchangeDoc { deviceId: string direction: Direction timestamp: Date sessionId: string cwmpMethod: string rawXml: string sequenceNumber: number createdAt: Date } function generateExchangesForDevice( deviceId: string, deviceIndex: number, count: number ): CwmpExchangeDoc[] { const exchanges: CwmpExchangeDoc[] = [] const now = Date.now() const twentyFourHoursMs = 24 * 60 * 60 * 1000 // Distribute exchanges over the last 24h let sequenceNumber = 1 let currentTimeMs = now - twentyFourHoursMs + randomInt(0, 60000) while (exchanges.length < count) { // Start a new session const sessionId = generateSessionId() // Each session has an Inform + some exchanges const sessionSize = Math.min(randomInt(4, 12), count - exchanges.length) // Always start session with Inform const informXml = generateInformXml(deviceIndex) const informTimestamp = new Date(currentTimeMs) exchanges.push({ deviceId, direction: 'CPE→ACS', timestamp: informTimestamp, sessionId, cwmpMethod: 'Inform', rawXml: informXml, sequenceNumber: sequenceNumber++, createdAt: informTimestamp, }) currentTimeMs += randomInt(50, 200) // 50-200ms between messages if (exchanges.length >= count) break // InformResponse const informRespTimestamp = new Date(currentTimeMs) exchanges.push({ deviceId, direction: 'ACS→CPE', timestamp: informRespTimestamp, sessionId, cwmpMethod: 'InformResponse', rawXml: generateInformResponseXml(), sequenceNumber: sequenceNumber++, createdAt: informRespTimestamp, }) currentTimeMs += randomInt(50, 200) // Fill rest of session with random patterns let remaining = sessionSize - 2 while (remaining > 0 && exchanges.length < count) { // Pick a random pattern (skip Inform pattern at index 0) const patternIndex = randomInt(1, SESSION_PATTERNS.length - 1) const pattern = SESSION_PATTERNS[patternIndex] for (const template of pattern) { if (exchanges.length >= count) break remaining-- const ts = new Date(currentTimeMs) exchanges.push({ deviceId, direction: template.direction, timestamp: ts, sessionId, cwmpMethod: template.cwmpMethod, rawXml: template.xmlGenerator(), sequenceNumber: sequenceNumber++, createdAt: ts, }) currentTimeMs += randomInt(50, 300) } } // Gap between sessions (30s to 30min) currentTimeMs += randomInt(30_000, 1_800_000) } return exchanges.slice(0, count) } // ─── Seeder Runner ────────────────────────────────────────────────────────── export async function seed(db: Db, config?: Partial): Promise { const cfg = { ...getConfig(), ...config } const collection = db.collection('cwmp_exchanges') const countersCollection = db.collection('cwmp_exchange_counters') console.log(`[Seeder] Seeding cwmp_exchanges with:`) console.log(` - ${cfg.deviceCount} devices`) console.log(` - ${cfg.exchangesMin}-${cfg.exchangesMax} exchanges per device`) // Clear existing data await collection.deleteMany({}) await countersCollection.deleteMany({}) console.log(`[Seeder] Cleared existing cwmp_exchanges and counters`) let totalInserted = 0 for (let i = 0; i < cfg.deviceCount; i++) { const deviceId = generateDeviceId(i) const exchangeCount = randomInt(cfg.exchangesMin, cfg.exchangesMax) const exchanges = generateExchangesForDevice(deviceId, i, exchangeCount) // Bulk insert exchanges if (exchanges.length > 0) { await collection.insertMany(exchanges) // Update counter for the device const maxSeq = exchanges[exchanges.length - 1].sequenceNumber await countersCollection.updateOne( { _id: deviceId as any }, { $set: { seq: maxSeq } }, { upsert: true } ) totalInserted += exchanges.length console.log(`[Seeder] Device "${deviceId}": ${exchanges.length} exchanges (seq 1-${maxSeq})`) } } console.log(`[Seeder] Done. Total exchanges inserted: ${totalInserted}`) } // ─── Standalone Execution ─────────────────────────────────────────────────── async function main(): Promise { const config = getConfig() console.log(`[Seeder] Connecting to MongoDB at ${config.mongoUri}/${config.dbName}...`) const client = new MongoClient(config.mongoUri) await client.connect() const db = client.db(config.dbName) try { await seed(db, config) } finally { await client.close() console.log(`[Seeder] MongoDB connection closed.`) } } // Run directly if executed as a script const isMainModule = process.argv[1]?.includes('cwmp_exchanges_seeder') if (isMainModule) { main().catch((err) => { console.error('[Seeder] Fatal error:', err) process.exit(1) }) }