trace + backup propertie

This commit is contained in:
Kevin L'Hereec committed 2026-06-01 16:20:58 +02:00
1 parent b3b1edec17
commit ea73e91aa4
8 files changed
+164 -37

No files matched your search

+18
View File
@@ -90,12 +90,30 @@ const routes = {
tokens: [{"old":"/api/v1/tr069-trace","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace","type":0,"val":"tr069-trace","end":""}],
types: placeholder as Registry['tr_069_trace.store']['types'],
},
'tr_069_trace.raw_config': {
methods: ["GET","HEAD"],
pattern: '/api/v1/tr069-trace/config',
tokens: [{"old":"/api/v1/tr069-trace/config","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"tr069-trace","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"config","end":""}],
types: placeholder as Registry['tr_069_trace.raw_config']['types'],
},
'tr_069_trace.save_raw_config': {
methods: ["PUT"],
pattern: '/api/v1/tr069-trace/config',
tokens: [{"old":"/api/v1/tr069-trace/config","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"tr069-trace","end":""},{"old":"/api/v1/tr069-trace/config","type":0,"val":"config","end":""}],
types: placeholder as Registry['tr_069_trace.save_raw_config']['types'],
},
'tr_069_trace.destroy': {
methods: ["DELETE"],
pattern: '/api/v1/tr069-trace/:id',
tokens: [{"old":"/api/v1/tr069-trace/:id","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace/:id","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace/:id","type":0,"val":"tr069-trace","end":""},{"old":"/api/v1/tr069-trace/:id","type":1,"val":"id","end":""}],
types: placeholder as Registry['tr_069_trace.destroy']['types'],
},
'tr_069_trace.show_log': {
methods: ["GET","HEAD"],
pattern: '/api/v1/tr069-trace/:id/log',
tokens: [{"old":"/api/v1/tr069-trace/:id/log","type":0,"val":"api","end":""},{"old":"/api/v1/tr069-trace/:id/log","type":0,"val":"v1","end":""},{"old":"/api/v1/tr069-trace/:id/log","type":0,"val":"tr069-trace","end":""},{"old":"/api/v1/tr069-trace/:id/log","type":1,"val":"id","end":""},{"old":"/api/v1/tr069-trace/:id/log","type":0,"val":"log","end":""}],
types: placeholder as Registry['tr_069_trace.show_log']['types'],
},
'configurations_v_1.index': {
methods: ["GET","HEAD"],
pattern: '/api/v1/configurations',
+36
View File
@@ -175,6 +175,30 @@ export interface Registry {
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/tr069_trace_controller').default['store']>>>
}
}
'tr_069_trace.raw_config': {
methods: ["GET","HEAD"]
pattern: '/api/v1/tr069-trace/config'
types: {
body: {}
paramsTuple: []
params: {}
query: {}
response: ExtractResponse<Awaited<ReturnType<import('#controllers/tr069_trace_controller').default['rawConfig']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/tr069_trace_controller').default['rawConfig']>>>
}
}
'tr_069_trace.save_raw_config': {
methods: ["PUT"]
pattern: '/api/v1/tr069-trace/config'
types: {
body: {}
paramsTuple: []
params: {}
query: {}
response: ExtractResponse<Awaited<ReturnType<import('#controllers/tr069_trace_controller').default['saveRawConfig']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/tr069_trace_controller').default['saveRawConfig']>>>
}
}
'tr_069_trace.destroy': {
methods: ["DELETE"]
pattern: '/api/v1/tr069-trace/:id'
@@ -187,6 +211,18 @@ export interface Registry {
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/tr069_trace_controller').default['destroy']>>>
}
}
'tr_069_trace.show_log': {
methods: ["GET","HEAD"]
pattern: '/api/v1/tr069-trace/:id/log'
types: {
body: {}
paramsTuple: [ParamValue]
params: { id: ParamValue }
query: {}
response: ExtractResponse<Awaited<ReturnType<import('#controllers/tr069_trace_controller').default['showLog']>>>
errorResponse: ExtractErrorResponse<Awaited<ReturnType<import('#controllers/tr069_trace_controller').default['showLog']>>>
}
}
'configurations_v_1.index': {
methods: ["GET","HEAD"]
pattern: '/api/v1/configurations'
+3
View File
@@ -24,7 +24,10 @@ export interface ApiDefinition {
tr069Trace: {
index: typeof routes['tr_069_trace.index']
store: typeof routes['tr_069_trace.store']
rawConfig: typeof routes['tr_069_trace.raw_config']
saveRawConfig: typeof routes['tr_069_trace.save_raw_config']
destroy: typeof routes['tr_069_trace.destroy']
showLog: typeof routes['tr_069_trace.show_log']
}
configurationsV1: {
index: typeof routes['configurations_v_1.index']
+8
View File
@@ -18,7 +18,10 @@ export type ScannedRoutes = {
'commands.store': { paramsTuple?: []; params?: {} }
'tr_069_trace.index': { paramsTuple?: []; params?: {} }
'tr_069_trace.store': { paramsTuple?: []; params?: {} }
'tr_069_trace.raw_config': { paramsTuple?: []; params?: {} }
'tr_069_trace.save_raw_config': { paramsTuple?: []; params?: {} }
'tr_069_trace.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'tr_069_trace.show_log': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'configurations_v_1.index': { paramsTuple?: []; params?: {} }
'configurations_v_1.list_tags': { paramsTuple?: []; params?: {} }
'configurations_v_1.store': { paramsTuple?: []; params?: {} }
@@ -81,6 +84,8 @@ export type ScannedRoutes = {
'commands.index': { paramsTuple?: []; params?: {} }
'commands.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'tr_069_trace.index': { paramsTuple?: []; params?: {} }
'tr_069_trace.raw_config': { paramsTuple?: []; params?: {} }
'tr_069_trace.show_log': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'configurations_v_1.index': { paramsTuple?: []; params?: {} }
'configurations_v_1.list_tags': { paramsTuple?: []; params?: {} }
'configurations_v_1.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
@@ -112,6 +117,8 @@ export type ScannedRoutes = {
'commands.index': { paramsTuple?: []; params?: {} }
'commands.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'tr_069_trace.index': { paramsTuple?: []; params?: {} }
'tr_069_trace.raw_config': { paramsTuple?: []; params?: {} }
'tr_069_trace.show_log': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'configurations_v_1.index': { paramsTuple?: []; params?: {} }
'configurations_v_1.list_tags': { paramsTuple?: []; params?: {} }
'configurations_v_1.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
@@ -142,6 +149,7 @@ export type ScannedRoutes = {
'firmware_catalog.delete_route': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
}
PUT: {
'tr_069_trace.save_raw_config': { paramsTuple?: []; params?: {} }
'configurations_v_1.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'firmware_catalog.update_entry': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
'firmware_catalog.update_route': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
+7
View File
@@ -3,6 +3,7 @@ import { buildSoapEnvelope } from '../protocol/soap.js'
import { CWMPMessageBuilder } from '../protocol/common/cwmp_message_builder.js'
import { StateMachineManager } from '../services/cwmp/state_machine_manager.js'
import { Device, detectDataModelFromParams } from '../domain/device/device.js'
import { reclassifyDeviceFromCatalog } from '../services/parameter_writability.js'
import { DeviceId } from '../domain/device/device_id.js'
import { DeviceParameter, DeviceProperty } from '../domain/device/device_parameter.js'
import DeviceModel from '../models/device.js'
@@ -120,6 +121,12 @@ export default class CwmpController {
if (paramValues.length > 0) {
device.updateFromValues(paramValues)
}
// Reclassify properties → parameters using catalog when GPN is unsupported
if (device.gpnStatus === 'unsupported') {
reclassifyDeviceFromCatalog(device)
}
device.setTmpParamValues(paramValues)
// Detect TR data model from Inform parameter names (Device.* = TR-181, InternetGatewayDevice.* = TR-098)
+3 -37
View File
@@ -1,18 +1,9 @@
import { Device } from '../../domain/device/device.js'
import { DeviceParameter } from '../../domain/device/device_parameter.js'
import { ParameterType } from '../../domain/shared/types.js'
import { CWMPTaskQueue, ResultStatus, TaskResult } from './task_queue.js'
import { normalizePath } from '../parameter_writability.js'
import { reclassifyDeviceFromCatalog } from '../parameter_writability.js'
import DeviceModel from '../../models/device.js'
import logger from '@adonisjs/core/services/logger'
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const tr181Catalog: Array<{ path: string; writable: boolean; deviceClass: string }> =
require('../../../config/tr181-catalog.json')
const catalogWritableIndex = new Set(
tr181Catalog.filter((e) => e.writable).map((e) => e.path)
)
// Parameter types (simplified from legacy provisioning domain)
interface SetParameterValue {
@@ -93,33 +84,8 @@ export class DeviceActionExecutor {
}
/**
* Reclassify device properties as parameters using the TR-181 catalog.
* Called when GPN is unsupported — properties that are writable per the catalog
* should be moved to parameters so the configuration engine can write to them.
* Get parameter values from device.
*/
private reclassifyFromCatalog(device: Device): void {
const properties = device.getProperties()
let reclassified = 0
for (const prop of properties) {
const normalized = normalizePath(prop.name)
if (catalogWritableIndex.has(normalized)) {
// Move from properties to parameters (writable)
const snap = prop.toSnapshot()
device.setParameter(
new DeviceParameter(snap.name, snap.value, snap.type, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList)
)
// Remove from properties map
;(device as any)._properties.delete(prop.name)
reclassified++
}
}
if (reclassified > 0) {
logger.debug({ identifier: device.identifier, reclassified }, '[DeviceAction] Reclassified properties → parameters via catalog')
}
}
async getParameterValues(
device: Device,
parameterPaths: string[],
@@ -208,7 +174,7 @@ export class DeviceActionExecutor {
if (code === 9000 || code === 9001) {
device.setGpnStatus('unsupported')
// Reclassify parameters using TR-181 catalog writability
this.reclassifyFromCatalog(device)
reclassifyDeviceFromCatalog(device)
await this.persistDevice(device)
}
// Autres erreurs (réseau, timeout, 9002+) → gpnStatus inchangé
+29
View File
@@ -1,5 +1,7 @@
import { createRequire } from 'node:module'
import type { DeviceSnapshot } from '../domain/device/device.js'
import type { Device } from '../domain/device/device.js'
import { DeviceParameter } from '../domain/device/device_parameter.js'
const require = createRequire(import.meta.url)
const tr181Catalog: Array<{ path: string; writable: boolean; deviceClass: string }> =
@@ -7,6 +9,9 @@ const tr181Catalog: Array<{ path: string; writable: boolean; deviceClass: string
// Index chargé une fois au démarrage — O(1) lookup
const catalogIndex = new Map(tr181Catalog.map((e) => [e.path, e.writable]))
const catalogWritableIndex = new Set(
tr181Catalog.filter((e) => e.writable).map((e) => e.path)
)
/** Remplace les segments numériques par {i} pour matcher les entrées du catalogue */
export function normalizePath(path: string): string {
@@ -46,3 +51,27 @@ export function resolveWritability(snapshot: DeviceSnapshot, paramPath: string):
if (gpnStatus === 'unsupported') return { writable: true, source: 'default' }
return { writable: null, source: 'unknown' }
}
/**
* Reclassify device properties as parameters using the TR-181 catalog.
* Properties that are writable per the catalog are moved to parameters.
* Should be called after updateFromValues when gpnStatus is 'unsupported'.
*/
export function reclassifyDeviceFromCatalog(device: Device): number {
const properties = device.getProperties()
let reclassified = 0
for (const prop of properties) {
const normalized = normalizePath(prop.name)
if (catalogWritableIndex.has(normalized)) {
const snap = prop.toSnapshot()
device.setParameter(
new DeviceParameter(snap.name, snap.value, snap.type, snap.createdAt, snap.updatedAt, snap.notification, snap.accessList)
)
;(device as any)._properties.delete(prop.name)
reclassified++
}
}
return reclassified
}
+60
View File
@@ -14,6 +14,51 @@ const { device, modelPaths, commands } = defineProps<{
const isLoading = ref(false)
const showDeleteConfirm = ref(false)
const isTraced = ref(false)
const tracingAction = ref(false)
// Check if device is already traced
async function checkTraceStatus() {
try {
const response = await fetch('/api/v1/tr069-trace')
if (response.ok) {
const data = await response.json()
const entries = data.entries || []
isTraced.value = entries.some((e: any) => e.deviceId === device.identifier)
}
} catch {
// Silently ignore
}
}
async function toggleTrace() {
tracingAction.value = true
try {
if (isTraced.value) {
const response = await fetch(`/api/v1/tr069-trace/${encodeURIComponent(device.identifier)}`, {
method: 'DELETE',
})
if (response.ok || response.status === 204) {
isTraced.value = false
}
} else {
const response = await fetch('/api/v1/tr069-trace', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ deviceId: device.identifier }),
})
if (response.ok) {
isTraced.value = true
}
}
} catch {
// Silently ignore
} finally {
tracingAction.value = false
}
}
checkTraceStatus()
function deleteDevice() {
router.delete(`/devices/${encodeURIComponent(device.identifier)}`, {
@@ -328,6 +373,21 @@ function getPropertyValue(name: string): string {
<button class="btn btn-outline-dark btn-sm" @click="openGetModelPopup" :disabled="isLoading">
{{ t('button.getModel') }}
</button>
<button
class="btn btn-sm"
:class="isTraced ? 'btn-warning' : 'btn-outline-primary'"
:disabled="tracingAction"
@click="toggleTrace"
>
<svg v-if="!isTraced" xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293z"/>
</svg>
{{ tracingAction ? '…' : (isTraced ? 'Remove TR-069 monitoring' : 'Add to TR-069 monitoring') }}
</button>
<button class="btn btn-outline-danger btn-sm ms-auto" @click="showDeleteConfirm = true">
Supprimer l'équipement
</button>