mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-04-19 08:36:14 +00:00
Use dmidecode to read cpuid in non ARM devices
Cpu id is set to null so far for non ARM devices (e.g. Intel NUC). This parses the output of dmidecode to get the cpu id and system model. Change-type: patch
This commit is contained in:
parent
c7fc7aacf8
commit
d06b8e053e
@ -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';
|
||||
@ -75,9 +76,9 @@ export async function getSystemId(): Promise<string | undefined> {
|
||||
return buffer.toString('utf-8').replace(/\0/g, '');
|
||||
} catch {
|
||||
// Otherwise use dmidecode
|
||||
const [baseBoardInfo] = (await dmidecode('baseboard')).filter(
|
||||
(entry) => entry.type === 'Base Board Information',
|
||||
);
|
||||
const [baseBoardInfo] = (
|
||||
await dmidecode('baseboard').catch(() => [] as DmiDecodeInfo[])
|
||||
).filter((entry) => entry.type === 'Base Board Information');
|
||||
return baseBoardInfo?.values?.['Serial Number'] || undefined;
|
||||
}
|
||||
}
|
||||
@ -88,9 +89,9 @@ export async function getSystemModel(): Promise<string | undefined> {
|
||||
// Remove the null byte at the end
|
||||
return buffer.toString('utf-8').replace(/\0/g, '');
|
||||
} catch {
|
||||
const [baseBoardInfo] = (await dmidecode('baseboard')).filter(
|
||||
(entry) => entry.type === 'Base Board Information',
|
||||
);
|
||||
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 (
|
||||
@ -104,51 +105,58 @@ export async function getSystemModel(): Promise<string | 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 async function dmidecode(t: string) {
|
||||
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 }), {}),
|
||||
}))
|
||||
);
|
||||
}
|
||||
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> {
|
||||
|
@ -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 () => {
|
||||
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 = [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user