Merge pull request #1864 from balena-os/dmidecode

Use dmidecode to read cpuid in non ARM devices
This commit is contained in:
bulldozer-balena[bot] 2022-01-19 15:28:57 +00:00 committed by GitHub
commit ec263d3028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 222 additions and 24 deletions

View File

@ -33,6 +33,7 @@ RUN apk add --no-cache \
libuv \
sqlite-libs \
sqlite-dev \
dmidecode \
dbus-dev
COPY build-utils/node-sums.txt .
@ -99,6 +100,7 @@ RUN apk add --no-cache \
avahi \
dbus \
libstdc++ \
dmidecode \
sqlite-libs
WORKDIR /usr/src/app

View File

@ -1,5 +1,6 @@
import * as systeminformation from 'systeminformation';
import * as _ from 'lodash';
import * as memoizee from 'memoizee';
import { promises as fs } from 'fs';
import { exec } from './fs-utils';
@ -67,16 +68,96 @@ export async function getCpuTemp(): Promise<number> {
return Math.round(tempInfo.main);
}
export async function getCpuId(): Promise<string | undefined> {
export async function getSystemId(): Promise<string | undefined> {
try {
// This will work on arm devices
const buffer = await fs.readFile('/proc/device-tree/serial-number');
// Remove the null byte at the end
return buffer.toString('utf-8').replace(/\0/g, '');
} catch {
return undefined;
// Otherwise use dmidecode
const [baseBoardInfo] = (
await dmidecode('baseboard').catch(() => [] as DmiDecodeInfo[])
).filter((entry) => entry.type === 'Base Board Information');
return baseBoardInfo?.values?.['Serial Number'] || undefined;
}
}
export async function getSystemModel(): Promise<string | undefined> {
try {
const buffer = await fs.readFile('/proc/device-tree/model');
// Remove the null byte at the end
return buffer.toString('utf-8').replace(/\0/g, '');
} catch {
const [baseBoardInfo] = (
await dmidecode('baseboard').catch(() => [] as DmiDecodeInfo[])
).filter((entry) => entry.type === 'Base Board Information');
// Join manufacturer and product name in a single string
return (
[
baseBoardInfo?.values?.['Manufacturer'],
baseBoardInfo?.values?.['Product Name'],
]
.filter((s) => !!s)
.join(' ') || undefined
);
}
}
export type DmiDecodeInfo = { type: string; values: { [key: string]: string } };
/**
* Parse the output of dmidecode and return an array of
* objects {type: string, values: string[]}
*
* This only parses simple key,value pairs from the output
* of dmidecode, multiline strings and arrays are ignored
*
* The output of the command is memoized
*/
export const dmidecode = memoizee(
async (t: string): Promise<DmiDecodeInfo[]> => {
const { stdout: info } = await exec(`dmidecode -t ${t}`);
return (
info
.toString()
.split(/\r?\n/) // Split by line jumps
// Split into groups by looking for empty lines
.reduce((groups, line) => {
const currentGroup = groups.pop() || [];
if (/^\s*$/.test(line)) {
// For each empty line create a new group
groups.push(currentGroup);
groups.push([]);
} else {
// Otherwise append the line to the group
currentGroup.push(line);
groups.push(currentGroup);
}
return groups;
}, [] as string[][])
// Only select the handles
.filter((group) => group.length > 1 && /^Handle/.test(group[0]))
.map(([, type, ...lines]) => ({
type,
values: lines
// Only select lines that match 'key: value', this will exclude multiline strings
// and arrays (we don't care about those for these purposes)
.filter((line) => /^\s+[^:]+: .+$/.test(line))
.map((line) => {
const [key, value] = line.split(':').map((s) => s.trim());
// Finally convert the lines into key value pairs
return { [key]: value };
})
// And merge
.reduce((vals, v) => ({ ...vals, ...v }), {}),
}))
);
},
{ promise: true },
);
const undervoltageRegex = /under.*voltage/i;
export async function undervoltageDetected(): Promise<boolean> {
try {
@ -110,7 +191,7 @@ export async function getSystemMetrics() {
getCpuUsage(),
getMemoryInformation(),
getCpuTemp(),
getCpuId(),
getSystemId(),
getStorageInfo(),
]);

View File

@ -95,11 +95,58 @@ describe('System information', () => {
// Make sure it's the right number given the mocked data
expect(tempInfo).to.equal(51);
});
});
it('gets CPU ID', async () => {
const cpuId = await sysInfo.getCpuId();
describe('baseboard information', () => {
afterEach(() => {
(fs.readFile as SinonStub).reset();
(fsUtils.exec as SinonStub).reset();
});
// Do these two tests first so the dmidecode call is not memoized yet
it('returns undefined system model if dmidecode throws', async () => {
(fs.readFile as SinonStub).rejects('Not found');
(fsUtils.exec as SinonStub).rejects('Something bad happened');
const systemModel = await sysInfo.getSystemModel();
expect(systemModel).to.be.undefined;
});
it('returns undefined system ID if dmidecode throws', async () => {
(fs.readFile as SinonStub).rejects('Not found');
(fsUtils.exec as SinonStub).rejects('Something bad happened');
const systemId = await sysInfo.getSystemId();
expect(systemId).to.be.undefined;
});
it('gets system ID', async () => {
(fs.readFile as SinonStub).resolves(mockCPU.idBuffer);
const cpuId = await sysInfo.getSystemId();
expect(cpuId).to.equal('1000000001b93f3f');
});
it('gets system ID from dmidecode if /proc/device-tree/serial-number is not available', async () => {
(fs.readFile as SinonStub).rejects('Not found');
(fsUtils.exec as SinonStub).resolves({
stdout: mockCPU.dmidecode,
});
const cpuId = await sysInfo.getSystemId();
expect(cpuId).to.equal('GEBN94600PWW');
});
it('gets system model', async () => {
(fs.readFile as SinonStub).resolves('Raspberry PI 4');
const systemModel = await sysInfo.getSystemModel();
expect(systemModel).to.equal('Raspberry PI 4');
});
it('gets system model from dmidecode if /proc/device-tree/model is not available', async () => {
(fs.readFile as SinonStub).rejects('Not found');
(fsUtils.exec as SinonStub).resolves({
stdout: mockCPU.dmidecode,
});
const systemModel = await sysInfo.getSystemModel();
expect(systemModel).to.equal('Intel Corporation NUC7i5BNB');
});
});
describe('getMemoryInformation', async () => {
@ -154,6 +201,51 @@ describe('System information', () => {
expect(await sysInfo.undervoltageDetected()).to.be.false;
});
});
describe('dmidecode', () => {
it('parses dmidecode output into json', async () => {
(fsUtils.exec as SinonStub).resolves({
stdout: mockCPU.dmidecode,
});
expect(await sysInfo.dmidecode('baseboard')).to.deep.equal([
{
type: 'Base Board Information',
values: {
Manufacturer: 'Intel Corporation',
'Product Name': 'NUC7i5BNB',
Version: 'J31144-313',
'Serial Number': 'GEBN94600PWW',
'Location In Chassis': 'Default string',
'Chassis Handle': '0x0003',
Type: 'Motherboard',
'Contained Object Handles': '0',
},
},
{
type: 'On Board Device 1 Information',
values: {
Type: 'Sound',
Status: 'Enabled',
Description: 'Realtek High Definition Audio Device',
},
},
{
type: 'Onboard Device',
values: {
'Reference Designation': 'Onboard - Other',
Type: 'Other',
Status: 'Enabled',
'Type Instance': '1',
'Bus Address': '0000',
},
},
]);
// Reset the stub
(fsUtils.exec as SinonStub).reset();
});
});
});
const mockCPU = {
@ -236,25 +328,48 @@ const mockCPU = {
},
],
},
idBuffer: Buffer.from([
0x31,
0x30,
0x30,
0x30,
0x30,
0x30,
0x30,
0x30,
0x30,
0x31,
0x62,
0x39,
0x33,
0x66,
0x33,
0x66,
0x00,
]),
idBuffer: Buffer.from('1000000001b93f3f'),
dmidecode: Buffer.from(`# dmidecode 3.3
Getting SMBIOS data from sysfs.
SMBIOS 3.1.1 present.
Handle 0x0002, DMI type 2, 15 bytes
Base Board Information
Manufacturer: Intel Corporation
Product Name: NUC7i5BNB
Version: J31144-313
Serial Number: GEBN94600PWW
Asset Tag:
Features:
Board is a hosting board
Board is replaceable
Location In Chassis: Default string
Chassis Handle: 0x0003
Type: Motherboard
Contained Object Handles: 0
Handle 0x000F, DMI type 10, 20 bytes
On Board Device 1 Information
Type: Video
Status: Enabled
Description: Intel(R) HD Graphics Device
On Board Device 2 Information
Type: Ethernet
Status: Enabled
Description: Intel(R) I219-V Gigabit Network Device
On Board Device 3 Information
Type: Sound
Status: Enabled
Description: Realtek High Definition Audio Device
Handle 0x003F, DMI type 41, 11 bytes
Onboard Device
Reference Designation: Onboard - Other
Type: Other
Status: Enabled
Type Instance: 1
Bus Address: 0000:00:00.0
`),
};
const mockFS = [
{