Files
adam-acs/app/services/cwmp/configuration_evaluation_state.ts
Kevin L'Hereec f9e56c78b1 refactor: migrate all internal imports to package.json subpath aliases
- Add #domain, #protocol, #utils aliases to package.json imports map
- Replace all relative imports (./, ../, ../..) in app/ and
  database/mongo_migrations/ with the corresponding #alias imports,
  eliminating relative path traversal across the codebase
- Remove dead code: ConfigurationVersionModel.findByConfigurationIdentifier
  (unused, superseded by paginateVersions) and unused exception classes
  (NotFoundException, UnprocessableEntityException, DatabaseException)
  from app/exceptions/index.ts, keeping only BadRequestException
- Fix pre-existing syntax error in configuration_version.ts (mismatched
  parenthesis) that broke tsc
- Fix eqeqeq violation in configuration_service.ts and isNaN ->
  Number.isNaN in configuration_engine.ts / cwmp-1-0.ts / exchange_controller.ts
- Fix no-shadow violation in cwmp-1-0.ts EventStruct constructor
- Prettier formatting pass on touched files

Validated: tsc --noEmit clean, eslint app/ clean, full test suite green.
2026-07-22 11:45:14 +02:00

396 lines
14 KiB
TypeScript

import ConfigurationEngine from '#services/configuration_engine'
import type { EngineResult } from '#services/configuration_engine'
import type { DeviceStateResult, DeviceContext } from '#services/cwmp/main_state_machine'
import { PartialOperationalState } from '#services/cwmp/main_state_machine'
import DeviceModel from '#models/device'
import type { GetParameterNamesResponse, GetParameterValuesResponse } from '#protocol/tr069_types'
import logger from '@adonisjs/core/services/logger'
import { ComplianceStatus } from '#domain/configuration/compliance'
import { buildProvisioningCode } from '#utils/provisioning_code'
/**
* ConfigurationEvaluationState
*
* Stateful state that drives the full configuration evaluation flow:
* 1. On first handle(): calls ConfigurationEngine.evaluate(), sets compliance to "in_progress"
* 2. Processes inventory reads one path at a time (GetParameterNames RPC)
* 3. Processes all parameter diffs in one SetParameterValues RPC
* 4. If firmware needed, transitions to FirmwareUpgradeState
* 5. Otherwise, transitions to ComplianceUpdateState
*/
export class ConfigurationEvaluationState {
name = 'configuration-evaluation'
private engine: ConfigurationEngine
private engineResult: EngineResult | null = null
private inventoryQueue: string[] = []
private pendingValuesQueue: string[] = []
private paramDiffs: Array<{ name: string; value: string | number | boolean; type?: string }> = []
private initialized = false
private awaitingPhase: 'gpn_response' | 'gpv_response' | null = null
constructor() {
this.engine = new ConfigurationEngine()
}
async handle(context: DeviceContext): Promise<DeviceStateResult> {
try {
if (!this.initialized) {
this.initialized = true
this.engineResult = await this.engine.evaluate(context.device)
logger.info(
{
identifier: context.device.identifier,
configId: this.engineResult.configuration?.identifier || null,
inventoryPaths: this.engineResult.inventoryPaths.length,
paramDiffs: this.engineResult.parameterDiffs.length,
firmwareSteps: this.engineResult.firmwareMigrationPath.length,
isAlreadyCompliant: this.engineResult.isAlreadyCompliant,
},
'[ConfigEval] Evaluation complete'
)
await DeviceModel.updateOne(
{ identifier: context.device.identifier },
{
'compliance.status': ComplianceStatus.IN_PROGRESS,
'compliance.lastEvaluatedAt': new Date(),
'compliance.configurationIdentifier':
this.engineResult.configuration?.identifier || null,
}
)
// Skip inventory when already compliant — no need to re-read parameters
if (!this.engineResult.isAlreadyCompliant) {
this.inventoryQueue = [...this.engineResult.inventoryPaths]
this.paramDiffs = this.engineResult.parameterDiffs.map((d) => ({
name: d.name,
value: d.targetValue,
type: d.type,
}))
}
}
return this.processNextAction(context)
} catch (error: any) {
logger.error(
{ identifier: context.device.identifier, error: error.message },
'[ConfigEval] Evaluation failed'
)
await DeviceModel.updateOne(
{ identifier: context.device.identifier },
{
'compliance.status': ComplianceStatus.FAILED,
'compliance.lastError': error.message,
'compliance.lastEvaluatedAt': new Date(),
}
).catch(() => {})
return {
nextState: new PartialOperationalState(context.device, {
reason: 'configuration_evaluation_failed',
blockingState: 'configuration-evaluation',
error: error.message,
}),
}
}
}
private async processNextAction(context: DeviceContext): Promise<DeviceStateResult> {
// --- Process pending response from previous RPC (inline, not via task callback) ---
if (this.awaitingPhase === 'gpn_response') {
this.awaitingPhase = null
if (context.message?.success && context.message.data) {
const gpnResp = context.message.data as GetParameterNamesResponse
const infoList = gpnResp?.ParameterList?.ParameterInfoStruct ?? []
const leafNames: string[] = infoList
.map((info) => {
const n = info?.Name
return typeof n === 'string'
? n
: n && typeof (n as { _?: string })._ === 'string'
? (n as { _: string })._
: null
})
.filter((name): name is string => !!name && !name.endsWith('.'))
if (leafNames.length > 0) {
this.pendingValuesQueue.push(...leafNames)
}
logger.debug(
{ identifier: context.device.identifier, leafCount: leafNames.length },
'[ConfigEval] Inventory GPN response processed inline'
)
} else if (context.message && !context.message.success) {
// GPN failed during inventory — record failure
context.device.recordGpnFailure()
logger.debug(
{ identifier: context.device.identifier },
'[ConfigEval] Inventory GPN failed — skipping remaining inventory'
)
this.inventoryQueue = []
}
} else if (this.awaitingPhase === 'gpv_response') {
this.awaitingPhase = null
if (context.message?.success && context.message.data) {
const gpvResp = context.message.data as GetParameterValuesResponse
const list = gpvResp?.ParameterList?.ParameterValueStruct ?? []
if (list.length > 0) {
const normalized = list.map((pv) => ({
...pv,
Name:
typeof pv?.Name === 'string' ? pv.Name : (pv?.Name as { _?: string })?._ || pv?.Name,
}))
context.device.updateFromValues(normalized)
logger.debug(
{ identifier: context.device.identifier, count: normalized.length },
'[ConfigEval] Inventory GPV values saved to device'
)
}
}
}
// 1. Inventory phase 1: GetParameterNames per subtree path
// Skip if device does not support GPN or circuit breaker is active
if (
this.inventoryQueue.length > 0 &&
context.device.gpnStatus !== 'unsupported' &&
context.device.isGpnAllowed()
) {
const path = this.inventoryQueue.shift()!
logger.debug(
{ identifier: context.device.identifier, path },
'[ConfigEval] Inventory GetParameterNames'
)
const response = await context.services.deviceAction.getParameterNames(
context.device,
path,
() => Promise.resolve('')
)
this.awaitingPhase = 'gpn_response'
return { response }
} else if (this.inventoryQueue.length > 0) {
// GPN unsupported or circuit breaker active — skip inventory
logger.debug(
{ identifier: context.device.identifier, skipped: this.inventoryQueue.length },
'[ConfigEval] Skipping inventory (GPN unsupported or circuit breaker active)'
)
this.inventoryQueue = []
}
// 2. Inventory phase 2: GetParameterValues for discovered leaf names
if (this.pendingValuesQueue.length > 0) {
const leaves = this.pendingValuesQueue.splice(0)
logger.debug(
{ identifier: context.device.identifier, count: leaves.length },
'[ConfigEval] Inventory GetParameterValues'
)
const response = await context.services.deviceAction.getParameterValues(
context.device,
leaves,
() => Promise.resolve('')
)
this.awaitingPhase = 'gpv_response'
return { response }
}
// 3. All parameter diffs in one SetParameterValues RPC
if (this.paramDiffs.length > 0) {
const diffs = this.paramDiffs.splice(0)
logger.debug(
{ identifier: context.device.identifier, count: diffs.length },
'[ConfigEval] Applying parameter diffs'
)
const params = diffs.map((d) => ({ parameterName: d.name, value: d.value, type: d.type }))
const response = await context.services.deviceAction.applySetParameterValues(
context.device,
params,
() => Promise.resolve('')
)
return { response }
}
// 4. Firmware upgrade (if needed and within schedule)
if (this.engineResult!.firmwareMigrationPath.length > 0 && !this.engineResult!.skipFirmware) {
logger.debug(
{
identifier: context.device.identifier,
steps: this.engineResult!.firmwareMigrationPath.length,
},
'[ConfigEval] Transitioning to firmware upgrade'
)
return {
nextState: new FirmwareUpgradeState(
this.engineResult!.firmwareMigrationPath,
0,
this.engineResult!
),
}
}
// 5. All done
return { nextState: new ComplianceUpdateState(this.engineResult!) }
}
}
/**
* FirmwareUpgradeState
*
* Sends a Download RPC for the current migration step.
* After the RPC, the session ends (device reboots and re-Informs for next step).
*/
export class FirmwareUpgradeState {
name = 'firmware-upgrade'
private migrationSteps: any[]
private currentStepIndex: number
private engineResult: EngineResult
constructor(migrationSteps: any[], currentStepIndex: number = 0, engineResult: EngineResult) {
this.migrationSteps = migrationSteps
this.currentStepIndex = currentStepIndex
this.engineResult = engineResult
}
async handle(context: DeviceContext): Promise<DeviceStateResult> {
const step = this.migrationSteps[this.currentStepIndex]
if (!step) {
return { nextState: new ComplianceUpdateState(this.engineResult) }
}
logger.debug(
{
identifier: context.device.identifier,
step: this.currentStepIndex + 1,
total: this.migrationSteps.length,
},
'[FirmwareUpgrade] Sending Download RPC'
)
try {
const response = await context.services.deviceAction.applyDownload(
context.device,
{
commandKey: `fw-upgrade-step${this.currentStepIndex}-${Date.now()}`,
fileType: '1 Firmware Upgrade Image',
url: step.downloadUrl,
fileSize: step.fileSize || 0,
targetFileName: '',
},
async () => ''
)
return { response }
} catch (error: any) {
logger.error(
{ identifier: context.device.identifier, error: error.message },
'[FirmwareUpgrade] Download RPC failed'
)
await DeviceModel.updateOne(
{ identifier: context.device.identifier },
{
'compliance.status': ComplianceStatus.FAILED,
'compliance.lastError': `Firmware upgrade failed: ${error.message}`,
'compliance.lastEvaluatedAt': new Date(),
}
).catch(() => {})
return { isComplete: true }
}
}
}
/**
* ComplianceUpdateState
*
* Finalizes device compliance:
* - Updates compliance status to "conforme" in DB
* - For policy "once": writes ProvisioningCode to device
* - Transitions to ExecutingCommandsState to process pending user commands
*/
export class ComplianceUpdateState {
name = 'compliance-update'
private engineResult: EngineResult
private started = false
constructor(engineResult: EngineResult) {
this.engineResult = engineResult
}
async handle(context: DeviceContext): Promise<DeviceStateResult> {
const { ExecutingCommandsState } = await import('./main_state_machine.js')
if (!this.started) {
this.started = true
const updateData: Record<string, any> = {
'compliance.status': ComplianceStatus.COMPLIANT,
'compliance.lastEvaluatedAt': new Date(),
'compliance.lastError': null,
}
if (this.engineResult.configuration) {
updateData['compliance.configurationIdentifier'] =
this.engineResult.configuration.identifier
}
// Always build and write provisioning code to device (functional for "once", informative for "maintain")
if (this.engineResult.configuration) {
const provCode = buildProvisioningCode(this.engineResult.configuration)
// Check if device already has the correct provisioning code — skip SPV if so
const allParams = [...context.device.getParameters(), ...context.device.getProperties()]
const currentPC = allParams.find((p: any) => /(^|\.)ProvisioningCode$/.test(p?.name))
const currentPCValue = currentPC?.value ?? ''
if (currentPCValue === provCode) {
logger.debug(
{ identifier: context.device.identifier, provCode },
'[ComplianceUpdate] Provisioning code already matches — skipping SPV'
)
updateData.provisioningCode = provCode
await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(
() => {}
)
return { nextState: new ExecutingCommandsState(true) }
}
updateData.provisioningCode = provCode
await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(
() => {}
)
context.device.tmpPC = provCode
logger.debug(
{
identifier: context.device.identifier,
provCode,
policy: this.engineResult.configuration.policy,
},
'[ComplianceUpdate] Writing provisioning code'
)
const response = await context.services.deviceAction.insertProvisioningCode(
context.device,
() => Promise.resolve('')
)
return { response }
}
await DeviceModel.updateOne({ identifier: context.device.identifier }, updateData).catch(
() => {}
)
logger.debug(
{ identifier: context.device.identifier },
'[ComplianceUpdate] Compliance finalized (no configuration)'
)
return { nextState: new ExecutingCommandsState(true) }
}
// Second call (after provCode write response): execute user commands
return { nextState: new ExecutingCommandsState(true) }
}
}