Track per-device traffic totals and recover inventory state
This commit is contained in:
@@ -123,6 +123,7 @@ test('traffic service preserves active rules and snapshot when replacement fails
|
||||
});
|
||||
|
||||
const first = await service.refresh();
|
||||
assert.equal(first.epoch, 'boot');
|
||||
assert.equal(first.generation, 'rules-a');
|
||||
assert.deepEqual(first.devices, [{
|
||||
ip: '192.168.50.7',
|
||||
@@ -175,3 +176,94 @@ test('traffic service preserves active rules and snapshot when replacement fails
|
||||
assert.deepEqual(timedOut.devices, first.devices);
|
||||
assert.ok(calls.every(([, , options]) => options.timeout === 2_000));
|
||||
});
|
||||
|
||||
test('traffic service finalizes a detached slot once and keeps epoch totals monotonic', async () => {
|
||||
const firstObservation = observation('192.168.50.7', '00:11:22:33:44:55');
|
||||
const secondObservation = observation('192.168.50.8', '00:11:22:33:44:66');
|
||||
const firstDevice = selectTrafficDevices([firstObservation])[0];
|
||||
const secondDevice = selectTrafficDevices([secondObservation])[0];
|
||||
let observed = {
|
||||
observedAt: '2026-08-07T12:00:00.000Z',
|
||||
observations: [firstObservation],
|
||||
error: null,
|
||||
};
|
||||
const values = {
|
||||
A: { upload: '100', download: '200' },
|
||||
B: { upload: '5', download: '7' },
|
||||
};
|
||||
const keys = { A: firstDevice.key, B: secondDevice.key };
|
||||
let failNextCounterRead = false;
|
||||
const run = (command, args) => {
|
||||
if (command !== 'iptables-save') return { status: 0, stdout: '', stderr: '' };
|
||||
if (failNextCounterRead) {
|
||||
failNextCounterRead = false;
|
||||
return { status: null, stdout: '', stderr: '', error: new Error('retired slot read failed') };
|
||||
}
|
||||
const direction = args.includes('raw') ? 'upload' : 'download';
|
||||
const tableChain = args.includes('raw') ? uploadChain : downloadChain;
|
||||
return {
|
||||
status: 0,
|
||||
stdout: ['A', 'B'].map((slot) => (
|
||||
`[1:${values[slot][direction]}] -A ${tableChain}_${slot} -m comment --comment "harbor-traffic:${keys[slot]}:${direction}" -j RETURN`
|
||||
)).join('\n'),
|
||||
stderr: '',
|
||||
};
|
||||
};
|
||||
const generations = ['epoch-1', 'rules-a', 'rules-b'];
|
||||
const service = createDeviceTrafficService({
|
||||
observe: async () => observed,
|
||||
uploadChain,
|
||||
downloadChain,
|
||||
bypassCidrs: [],
|
||||
run,
|
||||
nextGeneration: () => generations.shift(),
|
||||
});
|
||||
|
||||
const first = await service.refresh();
|
||||
assert.equal(first.epoch, 'epoch-1');
|
||||
assert.equal(first.generation, 'rules-a');
|
||||
assert.deepEqual(first.devices.map(({ mac, uploadBytes, downloadBytes }) => ({
|
||||
mac, uploadBytes, downloadBytes,
|
||||
})), [{
|
||||
mac: firstObservation.mac,
|
||||
uploadBytes: '100',
|
||||
downloadBytes: '200',
|
||||
}]);
|
||||
|
||||
values.A = { upload: '130', download: '240' };
|
||||
observed = {
|
||||
observedAt: '2026-08-07T12:01:00.000Z',
|
||||
observations: [secondObservation],
|
||||
error: null,
|
||||
};
|
||||
failNextCounterRead = true;
|
||||
const pending = await service.refresh();
|
||||
assert.equal(pending.epoch, 'epoch-1');
|
||||
assert.equal(pending.generation, 'rules-b');
|
||||
assert.match(pending.source.error, /retired slot read failed/);
|
||||
assert.deepEqual(pending.devices.map(({ mac, uploadBytes, downloadBytes }) => ({
|
||||
mac, uploadBytes, downloadBytes,
|
||||
})), [
|
||||
{ mac: firstObservation.mac, uploadBytes: '100', downloadBytes: '200' },
|
||||
{ mac: secondObservation.mac, uploadBytes: '5', downloadBytes: '7' },
|
||||
]);
|
||||
|
||||
const finalized = await service.refresh();
|
||||
assert.equal(finalized.generation, 'rules-b');
|
||||
assert.equal(finalized.source.error, null);
|
||||
assert.deepEqual(finalized.devices.map(({ mac, uploadBytes, downloadBytes }) => ({
|
||||
mac, uploadBytes, downloadBytes,
|
||||
})), [
|
||||
{ mac: firstObservation.mac, uploadBytes: '130', downloadBytes: '240' },
|
||||
{ mac: secondObservation.mac, uploadBytes: '5', downloadBytes: '7' },
|
||||
]);
|
||||
|
||||
values.B = { upload: '15', download: '17' };
|
||||
const polled = await service.refresh();
|
||||
assert.deepEqual(polled.devices.map(({ mac, uploadBytes, downloadBytes }) => ({
|
||||
mac, uploadBytes, downloadBytes,
|
||||
})), [
|
||||
{ mac: firstObservation.mac, uploadBytes: '130', downloadBytes: '240' },
|
||||
{ mac: secondObservation.mac, uploadBytes: '15', downloadBytes: '17' },
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user