Merge pull request #1451 from balena-io/device-metadata

Add device system information to state endpoint patch
This commit is contained in:
bulldozer-balena[bot] 2020-09-03 14:34:27 +00:00 committed by GitHub
commit ef7bb8f92e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 187 additions and 12 deletions

34
package-lock.json generated
View File

@ -613,6 +613,12 @@
"integrity": "sha1-qIc1gLOoS2msHmhzI7Ffu+uQR5o=",
"dev": true
},
"@types/os-utils": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@types/os-utils/-/os-utils-0.0.1.tgz",
"integrity": "sha512-r5a1I4xr8e25GFiLdpHV+jHUawtbmtuCQHR1P3SH/Y1gmRy0KLjBVGe5uUkODU/6GntoGPhOu05jv+sBdAVpog==",
"dev": true
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -1311,7 +1317,7 @@
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
@ -3370,7 +3376,7 @@
},
"stream-combiner": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz",
"resolved": "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz",
"integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=",
"dev": true,
"requires": {
@ -3716,7 +3722,7 @@
},
"event-stream": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
"dev": true,
"requires": {
@ -6204,7 +6210,7 @@
},
"map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"resolved": "http://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
"dev": true
},
@ -6393,7 +6399,7 @@
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
@ -6544,7 +6550,7 @@
"dependencies": {
"commander": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
"dev": true
},
@ -7151,6 +7157,11 @@
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"os-utils": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/os-utils/-/os-utils-0.0.14.tgz",
"integrity": "sha1-KeURaXsZgrjGJ3Ihdf45eX72QVY="
},
"osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
@ -7705,7 +7716,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@ -8584,7 +8595,7 @@
},
"split": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
"resolved": "http://registry.npmjs.org/split/-/split-0.3.3.tgz",
"integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
"dev": true,
"requires": {
@ -8692,7 +8703,7 @@
},
"stream-combiner": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
"resolved": "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
"integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=",
"dev": true,
"requires": {
@ -8842,6 +8853,11 @@
"has-flag": "^3.0.0"
}
},
"systeminformation": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-4.27.3.tgz",
"integrity": "sha512-0Nc8AYEK818h7FI+bbe/kj7xXsMD5zOHvO9alUqQH/G4MHXu5tHQfWqC/bzWOk4JtoQPhnyLgxMYncDA2eeSBw=="
},
"tapable": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz",

View File

@ -29,8 +29,10 @@
"dependencies": {
"dbus": "^1.0.7",
"mdns-resolver": "^1.0.0",
"os-utils": "0.0.14",
"semver": "^7.3.2",
"sqlite3": "^4.1.1"
"sqlite3": "^4.1.1",
"systeminformation": "^4.27.3"
},
"engines": {
"node": "^12.16.2",
@ -56,6 +58,7 @@
"@types/morgan": "^1.9.0",
"@types/mz": "0.0.32",
"@types/node": "^12.12.54",
"@types/os-utils": "0.0.1",
"@types/request": "^2.48.5",
"@types/rimraf": "^2.0.4",
"@types/rwlock": "^5.0.2",

View File

@ -11,6 +11,8 @@ import DeviceState from '../device-state';
import { CoreOptions } from 'request';
import * as url from 'url';
import * as sysInfo from '../lib/system-info';
// The exponential backoff starts at 15s
const MINIMUM_BACKOFF_DELAY = 15000;
@ -121,6 +123,39 @@ const getStateDiff = (): DeviceStatus => {
.value(),
};
const shouldReportSysInfo = (type: string, past: number, now: number) => {
if (!past) {
return true;
}
// TODO: Deduplicate this code
switch (type) {
case 'cpu_usage':
// The bucket size of cpu usage is 20
return Math.floor(past / 20) !== Math.floor(now / 20);
case 'cpu_temp':
return Math.floor(past / 5) !== Math.floor(now / 5);
case 'memory_usage':
return Math.floor(past / 10) !== Math.floor(now / 10);
}
return true;
};
const toOmit: string[] = [];
_.each(diff.local, (value, key) => {
// if we have some system information that has changed, we check that it's
// within a certain range before reporting
if (
!shouldReportSysInfo(
key,
(lastReportedLocal as any)[key],
value as number,
)
) {
toOmit.push(key);
}
});
diff.local = _.omit(diff.local, toOmit);
return _.omitBy(diff, _.isEmpty);
};
@ -171,8 +206,16 @@ const reportCurrentState = (): null => {
reportPending = true;
try {
const currentDeviceState = await deviceState.getStatus();
_.assign(stateForReport.local, currentDeviceState.local);
_.assign(stateForReport.dependent, currentDeviceState.dependent);
const info = await sysInfo.getSysInfoToReport();
stateForReport.local = {
...stateForReport.local,
...currentDeviceState.local,
...info,
};
stateForReport.dependent = {
...stateForReport.dependent,
...currentDeviceState.dependent,
};
const stateDiff = getStateDiff();
if (_.size(stateDiff) === 0) {

113
src/lib/system-info.ts Normal file
View File

@ -0,0 +1,113 @@
import * as systeminformation from 'systeminformation';
import * as osUtils from 'os-utils';
import { fs, child_process } from 'mz';
export function getCpuUsage() {
return new Promise((resolve) => {
osUtils.cpuUsage((percent) => {
resolve(Math.round(percent * 100));
});
});
}
const blockDeviceRegex = /(\/dev\/.*)p\d+/;
export async function getStorageInfo() {
const fsInfo = await systeminformation.fsSize();
let mainFs: string | undefined;
let total = 0;
// First we find the block device which the data partition is part of
for (const partition of fsInfo) {
if (partition.mount === '/data') {
const match = partition.fs.match(blockDeviceRegex);
if (match == null) {
mainFs = undefined;
} else {
mainFs = match[1];
total = partition.size;
}
break;
}
}
if (!mainFs) {
return {
blockDevice: '',
storageUsed: undefined,
storageTotal: undefined,
};
}
let used = 0;
for (const partition of fsInfo) {
if (partition.fs.startsWith(mainFs)) {
used += partition.used;
}
}
return {
blockDevice: mainFs,
storageUsed: bytesToMb(used),
storageTotal: bytesToMb(total),
};
}
export async function getMemoryInformation() {
const mem = await systeminformation.mem();
return {
used: bytesToMb(mem.used),
total: bytesToMb(mem.total),
};
}
export async function getCpuTemp() {
return Math.round((await systeminformation.cpuTemperature()).main);
}
export async function getCpuId() {
// Read /proc/device-tree/serial-number
// if it's not there, return undefined
try {
const buffer = await fs.readFile('/proc/device-tree/serial-number');
// Remove the null byte at the end
return buffer.toString('utf-8').substr(0, buffer.length - 2);
} catch {
return undefined;
}
}
const undervoltageRegex = /under.*voltage/;
export async function undervoltageDetected() {
try {
const [dmesgStdout] = await child_process.exec('dmesg');
return undervoltageRegex.test(dmesgStdout.toString());
} catch {
return false;
}
}
export async function getSysInfoToReport() {
const [cpu, mem, temp, cpuid, storage, undervoltage] = await Promise.all([
getCpuUsage(),
getMemoryInformation(),
getCpuTemp(),
getCpuId(),
getStorageInfo(),
undervoltageDetected(),
]);
return {
cpu_usage: cpu,
memory_usage: mem.used,
memory_total: mem.total,
storage_usage: storage.storageUsed,
storage_total: storage.storageTotal,
storage_block_device: storage.blockDevice,
cpu_temp: temp,
cpu_id: cpuid,
is_undervolted: undervoltage,
};
}
function bytesToMb(bytes: number) {
return Math.floor(bytes / 1024 / 1024);
}