mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-22 15:02:23 +00:00
49ee1042a8
As the Supervisor is a privileged container, it has access to host /dev, and therefore has access to boot, data, and state balenaOS partitions. This commit sets up the framework for the following: - Finds the /dev partition that corresponds to each partition based on partition label - Mounts the partitions into set mountpoints in the device - Removes reliance on env vars and mountpoints provided by host's start-balena-supervisor script - Simplifies host path querying by centralizing these queries through methods in lib/host-utils.ts This particular changes env vars for and mounts the boot partition. Since the Supervisor would no longer rely on container `run` arguments provided by a host script, this change moves Supervisor closer to being able to start itself (Supervisor-as-an-app). Change-type: minor Signed-off-by: Christina Ying Wang <christina@balena.io>
856 lines
23 KiB
TypeScript
856 lines
23 KiB
TypeScript
import { stripIndent } from 'common-tags';
|
|
import { promises as fs } from 'fs';
|
|
import * as path from 'path';
|
|
import { SinonStub, stub, spy, SinonSpy } from 'sinon';
|
|
import { expect } from 'chai';
|
|
|
|
import * as deviceConfig from '~/src/device-config';
|
|
import * as fsUtils from '~/lib/fs-utils';
|
|
import * as logger from '~/src/logger';
|
|
import { Extlinux } from '~/src/config/backends/extlinux';
|
|
import { ConfigTxt } from '~/src/config/backends/config-txt';
|
|
import { Odmdata } from '~/src/config/backends/odmdata';
|
|
import { ConfigFs } from '~/src/config/backends/config-fs';
|
|
import { SplashImage } from '~/src/config/backends/splash-image';
|
|
import { pathOnBoot, pathOnRoot } from '~/lib/host-utils';
|
|
import {
|
|
configJsonPath as configJson,
|
|
hostOSVersionPath as osRelease,
|
|
} from '~/src/lib/constants';
|
|
|
|
import { testfs } from 'mocha-pod';
|
|
|
|
const extlinuxBackend = new Extlinux();
|
|
const configTxtBackend = new ConfigTxt();
|
|
const odmdataBackend = new Odmdata();
|
|
const configFsBackend = new ConfigFs();
|
|
const splashImageBackend = new SplashImage();
|
|
|
|
// TODO: Since the getBootConfig method is simple enough
|
|
// these tests could probably be removed if each backend has its own
|
|
// test and the src/config/utils module is properly tested.
|
|
describe('device-config', () => {
|
|
const configFsJson = pathOnBoot('configfs.json');
|
|
const configTxt = pathOnBoot('config.txt');
|
|
const deviceTypeJson = pathOnBoot('device-type.json');
|
|
|
|
let logSpy: SinonSpy;
|
|
|
|
before(async () => {
|
|
logSpy = spy(logger, 'logSystemMessage');
|
|
});
|
|
|
|
after(() => {
|
|
logSpy.restore();
|
|
});
|
|
|
|
afterEach(() => {
|
|
logSpy.resetHistory();
|
|
});
|
|
|
|
describe('formatConfigKeys', () => {
|
|
it('accepts RESIN_ and BALENA_ variables', async () => {
|
|
return expect(
|
|
deviceConfig.formatConfigKeys({
|
|
FOO: 'bar', // should be removed
|
|
BAR: 'baz', // should be removed
|
|
RESIN_SUPERVISOR_LOCAL_MODE: 'false', // any device
|
|
BALENA_SUPERVISOR_OVERRIDE_LOCK: 'false', // any device
|
|
BALENA_SUPERVISOR_POLL_INTERVAL: '100', // any device
|
|
RESIN_HOST_CONFIG_dtparam: 'i2c_arm=on","spi=on","audio=on', // config.txt backend
|
|
RESIN_HOST_CONFIGFS_ssdt: 'spidev1.0', // configfs backend
|
|
BALENA_HOST_EXTLINUX_isolcpus: '1,2,3', // extlinux backend
|
|
}),
|
|
).to.deep.equal({
|
|
SUPERVISOR_LOCAL_MODE: 'false',
|
|
SUPERVISOR_OVERRIDE_LOCK: 'false',
|
|
SUPERVISOR_POLL_INTERVAL: '100',
|
|
HOST_CONFIG_dtparam: 'i2c_arm=on","spi=on","audio=on',
|
|
HOST_CONFIGFS_ssdt: 'spidev1.0',
|
|
HOST_EXTLINUX_isolcpus: '1,2,3',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getDefaults', () => {
|
|
it('returns default configuration values', () => {
|
|
const conf = deviceConfig.getDefaults();
|
|
return expect(conf).to.deep.equal({
|
|
HOST_FIREWALL_MODE: 'off',
|
|
HOST_DISCOVERABILITY: 'true',
|
|
SUPERVISOR_VPN_CONTROL: 'true',
|
|
SUPERVISOR_POLL_INTERVAL: '900000',
|
|
SUPERVISOR_LOCAL_MODE: 'false',
|
|
SUPERVISOR_CONNECTIVITY_CHECK: 'true',
|
|
SUPERVISOR_LOG_CONTROL: 'true',
|
|
SUPERVISOR_DELTA: 'false',
|
|
SUPERVISOR_DELTA_REQUEST_TIMEOUT: '59000',
|
|
SUPERVISOR_DELTA_APPLY_TIMEOUT: '0',
|
|
SUPERVISOR_DELTA_RETRY_COUNT: '30',
|
|
SUPERVISOR_DELTA_RETRY_INTERVAL: '10000',
|
|
SUPERVISOR_DELTA_VERSION: '2',
|
|
SUPERVISOR_INSTANT_UPDATE_TRIGGER: 'true',
|
|
SUPERVISOR_OVERRIDE_LOCK: 'false',
|
|
SUPERVISOR_PERSISTENT_LOGGING: 'false',
|
|
SUPERVISOR_HARDWARE_METRICS: 'true',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('config.txt', () => {
|
|
const tFs = testfs({
|
|
// This is only needed so config.get doesn't fail
|
|
[configJson]: JSON.stringify({ deviceType: 'fincm3' }),
|
|
[configTxt]: stripIndent`
|
|
enable_uart=1
|
|
dtparam=i2c_arm=on
|
|
dtparam=spi=on
|
|
disable_splash=1
|
|
avoid_warnings=1
|
|
dtparam=audio=on
|
|
gpu_mem=16`,
|
|
[osRelease]: stripIndent`
|
|
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
|
META_BALENA_VERSION="2.88.5"
|
|
VERSION="2.88.5+rev1"
|
|
VARIANT_ID="dev"
|
|
`,
|
|
[deviceTypeJson]: JSON.stringify({
|
|
slug: 'fincm3',
|
|
arch: 'armv7hf',
|
|
}),
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await tFs.enable();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Reset the state of the fs after each test to
|
|
// prevent tests leaking into each other
|
|
await tFs.restore();
|
|
});
|
|
|
|
it('correctly parses a config.txt file', async () => {
|
|
// Will try to parse /test/data/mnt/boot/config.txt
|
|
await expect(
|
|
deviceConfig.getBootConfig(configTxtBackend),
|
|
).to.eventually.deep.equal({
|
|
HOST_CONFIG_dtparam: '"i2c_arm=on","spi=on","audio=on"',
|
|
HOST_CONFIG_enable_uart: '1',
|
|
HOST_CONFIG_disable_splash: '1',
|
|
HOST_CONFIG_avoid_warnings: '1',
|
|
HOST_CONFIG_gpu_mem: '16',
|
|
});
|
|
|
|
// Update config.txt to include initramfs and array variables
|
|
await fs.writeFile(
|
|
configTxt,
|
|
stripIndent`
|
|
initramfs initramf.gz 0x00800000
|
|
dtparam=i2c=on
|
|
dtparam=audio=on
|
|
dtoverlay=ads7846
|
|
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13
|
|
foobar=baz`,
|
|
);
|
|
|
|
await expect(
|
|
deviceConfig.getBootConfig(configTxtBackend),
|
|
).to.eventually.deep.equal({
|
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
|
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
|
|
HOST_CONFIG_dtoverlay:
|
|
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
|
HOST_CONFIG_foobar: 'baz',
|
|
});
|
|
});
|
|
|
|
it('does not allow setting forbidden keys', async () => {
|
|
const current = {
|
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
|
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
|
|
HOST_CONFIG_dtoverlay:
|
|
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
|
HOST_CONFIG_foobar: 'baz',
|
|
};
|
|
// Create another target with only change being initramfs which is blacklisted
|
|
const target = {
|
|
...current,
|
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00810000',
|
|
};
|
|
|
|
expect(() =>
|
|
// @ts-expect-error accessing private value
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configTxtBackend,
|
|
current,
|
|
target,
|
|
),
|
|
).to.throw('Attempt to change blacklisted config value initramfs');
|
|
|
|
// Check if logs were called
|
|
expect(logSpy).to.be.calledOnce;
|
|
expect(logSpy).to.be.calledWith(
|
|
'Attempt to change blacklisted config value initramfs',
|
|
{
|
|
error: 'Attempt to change blacklisted config value initramfs',
|
|
},
|
|
'Apply boot config error',
|
|
);
|
|
});
|
|
|
|
it('does not try to change config.txt if it should not change', async () => {
|
|
const current = {
|
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
|
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
|
|
HOST_CONFIG_dtoverlay:
|
|
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
|
HOST_CONFIG_foobar: 'baz',
|
|
};
|
|
const target = {
|
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
|
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
|
|
HOST_CONFIG_dtoverlay:
|
|
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
|
HOST_CONFIG_foobar: 'baz',
|
|
};
|
|
|
|
expect(
|
|
// @ts-expect-error accessing private value
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configTxtBackend,
|
|
current,
|
|
target,
|
|
),
|
|
).to.equal(false);
|
|
expect(logSpy).to.not.be.called;
|
|
});
|
|
|
|
it('writes the target config.txt', async () => {
|
|
const current = {
|
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
|
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
|
|
HOST_CONFIG_dtoverlay:
|
|
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
|
HOST_CONFIG_foobar: 'baz',
|
|
};
|
|
const target = {
|
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
|
HOST_CONFIG_dtparam: '"i2c=on","audio=off"',
|
|
HOST_CONFIG_dtoverlay: '"lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
|
HOST_CONFIG_foobar: 'bat',
|
|
HOST_CONFIG_foobaz: 'bar',
|
|
};
|
|
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configTxtBackend,
|
|
current,
|
|
target,
|
|
'fincm3',
|
|
),
|
|
).to.equal(true);
|
|
|
|
await deviceConfig.setBootConfig(configTxtBackend, target);
|
|
expect(logSpy).to.be.calledTwice;
|
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
|
expect(await fs.readFile(configTxt, 'utf-8')).to.equal(
|
|
stripIndent`
|
|
initramfs initramf.gz 0x00800000
|
|
dtparam=i2c=on
|
|
dtparam=audio=off
|
|
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13
|
|
dtoverlay=balena-fin
|
|
foobar=bat
|
|
foobaz=bar
|
|
` + '\n', // add newline because stripIndent trims last newline
|
|
);
|
|
});
|
|
|
|
it('ensures required fields are written to config.txt', async () => {
|
|
const current = {
|
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
|
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
|
|
HOST_CONFIG_dtoverlay:
|
|
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
|
HOST_CONFIG_foobar: 'baz',
|
|
};
|
|
const target = {
|
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
|
HOST_CONFIG_dtparam: '"i2c=on","audio=off"',
|
|
HOST_CONFIG_dtoverlay: '"lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
|
HOST_CONFIG_foobar: 'bat',
|
|
HOST_CONFIG_foobaz: 'bar',
|
|
};
|
|
|
|
expect(
|
|
// @ts-expect-error accessing private value
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configTxtBackend,
|
|
current,
|
|
target,
|
|
),
|
|
).to.equal(true);
|
|
|
|
await deviceConfig.setBootConfig(configTxtBackend, target);
|
|
expect(logSpy).to.be.calledTwice;
|
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
|
expect(await fs.readFile(configTxt, 'utf-8')).to.equal(
|
|
stripIndent`
|
|
initramfs initramf.gz 0x00800000
|
|
dtparam=i2c=on
|
|
dtparam=audio=off
|
|
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13
|
|
dtoverlay=balena-fin
|
|
foobar=bat
|
|
foobaz=bar
|
|
` + '\n', // add newline because stripIndent trims last newline
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('extlinux', () => {
|
|
const extlinuxConf = pathOnBoot('extlinux/extlinux.conf');
|
|
|
|
const tFs = testfs({
|
|
// This is only needed so config.get doesn't fail
|
|
[configJson]: JSON.stringify({}),
|
|
[osRelease]: stripIndent`
|
|
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
|
META_BALENA_VERSION="2.88.5"
|
|
VERSION="2.88.5+rev1"
|
|
VARIANT_ID="dev"
|
|
`,
|
|
[deviceTypeJson]: JSON.stringify({
|
|
slug: 'fincm3',
|
|
arch: 'armv7hf',
|
|
}),
|
|
[extlinuxConf]: stripIndent`
|
|
DEFAULT primary
|
|
TIMEOUT 30
|
|
MENU TITLE Boot Options
|
|
LABEL primary
|
|
MENU LABEL primary Image
|
|
LINUX /Image
|
|
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait
|
|
`,
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await tFs.enable();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Reset the state of the fs after each test to
|
|
// prevent tests leaking into each other
|
|
await tFs.restore();
|
|
});
|
|
|
|
it('should correctly write to extlinux.conf files', async () => {
|
|
const target = {
|
|
HOST_EXTLINUX_isolcpus: '2',
|
|
HOST_EXTLINUX_fdt: '/boot/mycustomdtb.dtb',
|
|
};
|
|
|
|
await deviceConfig.setBootConfig(extlinuxBackend, target);
|
|
expect(logSpy).to.be.calledTwice;
|
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
|
expect(await fs.readFile(extlinuxConf, 'utf-8')).to.equal(
|
|
stripIndent`
|
|
DEFAULT primary
|
|
TIMEOUT 30
|
|
MENU TITLE Boot Options
|
|
LABEL primary
|
|
MENU LABEL primary Image
|
|
LINUX /Image
|
|
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2
|
|
FDT /boot/mycustomdtb.dtb
|
|
` + '\n', // add newline because stripIndent trims last newline
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Balena fin', () => {
|
|
it('should always add the balena-fin dtoverlay', () => {
|
|
expect(configTxtBackend.ensureRequiredConfig('fincm3', {})).to.deep.equal(
|
|
{
|
|
dtoverlay: ['balena-fin'],
|
|
},
|
|
);
|
|
|
|
expect(
|
|
configTxtBackend.ensureRequiredConfig('fincm3', {
|
|
test: '123',
|
|
test2: ['123'],
|
|
test3: ['123', '234'],
|
|
}),
|
|
).to.deep.equal({
|
|
test: '123',
|
|
test2: ['123'],
|
|
test3: ['123', '234'],
|
|
dtoverlay: ['balena-fin'],
|
|
});
|
|
expect(
|
|
configTxtBackend.ensureRequiredConfig('fincm3', {
|
|
dtoverlay: 'test',
|
|
}),
|
|
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
|
|
expect(
|
|
configTxtBackend.ensureRequiredConfig('fincm3', {
|
|
dtoverlay: ['test'],
|
|
}),
|
|
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
|
|
});
|
|
|
|
it('should not cause a config change when the cloud does not specify the balena-fin overlay', () => {
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configTxtBackend,
|
|
{ HOST_CONFIG_dtoverlay: '"test","balena-fin"' },
|
|
{ HOST_CONFIG_dtoverlay: '"test"' },
|
|
'fincm3',
|
|
),
|
|
).to.equal(false);
|
|
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configTxtBackend,
|
|
{ HOST_CONFIG_dtoverlay: '"test","balena-fin"' },
|
|
{ HOST_CONFIG_dtoverlay: 'test' },
|
|
'fincm3',
|
|
),
|
|
).to.equal(false);
|
|
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configTxtBackend,
|
|
{ HOST_CONFIG_dtoverlay: '"test","test2","balena-fin"' },
|
|
{ HOST_CONFIG_dtoverlay: '"test","test2"' },
|
|
'fincm3',
|
|
),
|
|
).to.equal(false);
|
|
});
|
|
});
|
|
|
|
describe('ODMDATA', () => {
|
|
it('requires change when target is different', () => {
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
odmdataBackend,
|
|
{ HOST_ODMDATA_configuration: '2' },
|
|
{ HOST_ODMDATA_configuration: '5' },
|
|
'jetson-tx2',
|
|
),
|
|
).to.equal(true);
|
|
});
|
|
it('requires change when no target is set', () => {
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
odmdataBackend,
|
|
{ HOST_ODMDATA_configuration: '2' },
|
|
{},
|
|
'jetson-tx2',
|
|
),
|
|
).to.equal(false);
|
|
});
|
|
});
|
|
|
|
describe('config-fs', () => {
|
|
const acpiTables = pathOnRoot('boot/acpi-tables');
|
|
const sysKernelAcpiTable = pathOnRoot('sys/kernel/config/acpi/table');
|
|
|
|
const tFs = testfs({
|
|
// This is only needed so config.get doesn't fail
|
|
[configJson]: JSON.stringify({}),
|
|
[osRelease]: stripIndent`
|
|
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
|
META_BALENA_VERSION="2.88.5"
|
|
VERSION="2.88.5+rev1"
|
|
VARIANT_ID="dev"
|
|
`,
|
|
[configFsJson]: JSON.stringify({
|
|
ssdt: ['spidev1.1'],
|
|
}),
|
|
|
|
[deviceTypeJson]: JSON.stringify({
|
|
slug: 'fincm3',
|
|
arch: 'armv7hf',
|
|
}),
|
|
[acpiTables]: {
|
|
'spidev1.0.aml': '',
|
|
'spidev1.1.aml': '',
|
|
},
|
|
[sysKernelAcpiTable]: {
|
|
// Add necessary files to avoid the module reporting an error
|
|
'spidev1.1': {
|
|
oem_id: '',
|
|
oem_table_id: '',
|
|
oem_revision: '',
|
|
},
|
|
},
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await tFs.enable();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Reset the state of the fs after each test to
|
|
// prevent tests leaking into each other
|
|
await tFs.restore();
|
|
});
|
|
|
|
it('should correctly write to configfs.json files', async () => {
|
|
const current = {};
|
|
const target = {
|
|
HOST_CONFIGFS_ssdt: 'spidev1.0',
|
|
};
|
|
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configFsBackend,
|
|
current,
|
|
target,
|
|
'up-board',
|
|
),
|
|
).to.equal(true);
|
|
|
|
await deviceConfig.setBootConfig(configFsBackend, target);
|
|
expect(logSpy).to.be.calledTwice;
|
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
|
expect(await fs.readFile(configFsJson, 'utf-8')).to.equal(
|
|
'{"ssdt":["spidev1.0"]}',
|
|
);
|
|
});
|
|
|
|
// This test is skipped because it depends on internal details of the config-fs
|
|
// backend. We need to refactor that module to make this more testable
|
|
// properly
|
|
it.skip('should correctly load the configfs.json file', async () => {
|
|
await configFsBackend.initialise();
|
|
|
|
stub(fsUtils, 'exec').resolves();
|
|
await configFsBackend.initialise();
|
|
|
|
// TODO: unfortunately there is no way to test initialization without stubbing
|
|
// modprobe cannot be replaced by something else and the up-board modules
|
|
// will not be present within the container environment. OS tests need
|
|
// to check that applying configurations actually succeds
|
|
expect(fsUtils.exec).to.be.calledWith('modprobe acpi_configfs');
|
|
|
|
// If the module performs this call, it's because all the prior checks succeeded
|
|
expect(fsUtils.exec).to.be.calledWith(
|
|
'cat /mnt/root/boot/acpi-tables/spidev1.1.aml > /mnt/root/sys/kernel/config/acpi/table/spidev1.1/aml',
|
|
);
|
|
|
|
// Restore stubs
|
|
(fsUtils.exec as SinonStub).restore();
|
|
});
|
|
|
|
it('requires change when target is different', () => {
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configFsBackend,
|
|
{ HOST_CONFIGFS_ssdt: '' },
|
|
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
|
|
'up-board',
|
|
),
|
|
).to.equal(true);
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configFsBackend,
|
|
{ HOST_CONFIGFS_ssdt: '' },
|
|
{ HOST_CONFIGFS_ssdt: '"spidev1.0"' },
|
|
'up-board',
|
|
),
|
|
).to.equal(true);
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configFsBackend,
|
|
{ HOST_CONFIGFS_ssdt: '"spidev1.0"' },
|
|
{ HOST_CONFIGFS_ssdt: '"spidev1.0","spidev1.1"' },
|
|
'up-board',
|
|
),
|
|
).to.equal(true);
|
|
});
|
|
|
|
it('should not report change when target is equal to current', () => {
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configFsBackend,
|
|
{ HOST_CONFIGFS_ssdt: '' },
|
|
{ HOST_CONFIGFS_ssdt: '' },
|
|
'up-board',
|
|
),
|
|
).to.equal(false);
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configFsBackend,
|
|
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
|
|
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
|
|
'up-board',
|
|
),
|
|
).to.equal(false);
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configFsBackend,
|
|
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
|
|
{ HOST_CONFIGFS_ssdt: '"spidev1.0"' },
|
|
'up-board',
|
|
),
|
|
).to.equal(false);
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
configFsBackend,
|
|
{ HOST_CONFIGFS_ssdt: '"spidev1.0"' },
|
|
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
|
|
'up-board',
|
|
),
|
|
).to.equal(false);
|
|
});
|
|
});
|
|
|
|
describe('splash config', () => {
|
|
const defaultLogo =
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/wQDLA+84AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
|
|
const png =
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=';
|
|
const uri = `data:image/png;base64,${png}`;
|
|
|
|
const splash = pathOnBoot('splash');
|
|
|
|
const mockFs = testfs(
|
|
{
|
|
// This is only needed so config.get doesn't fail
|
|
[configJson]: JSON.stringify({}),
|
|
[osRelease]: stripIndent`
|
|
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
|
META_BALENA_VERSION="2.88.5"
|
|
VERSION="2.88.5+rev1"
|
|
VARIANT_ID="dev"
|
|
`,
|
|
[deviceTypeJson]: JSON.stringify({
|
|
slug: 'raspberrypi4-64',
|
|
arch: 'aarch64',
|
|
}),
|
|
[splash]: {
|
|
dummy: '', // to ensure the directory is created
|
|
},
|
|
},
|
|
{ cleanup: [`${splash}/*.png`] },
|
|
);
|
|
|
|
beforeEach(async () => {
|
|
await mockFs.enable();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await mockFs.restore();
|
|
});
|
|
|
|
it('should correctly write to resin-logo.png', async () => {
|
|
// Devices with balenaOS < 2.51 use resin-logo.png
|
|
// Devices with balenaOS >= 2.51 use balena-logo.png
|
|
const tTfs = await testfs({
|
|
[splash]: {
|
|
'resin-logo.png': Buffer.from(png, 'base64'),
|
|
},
|
|
}).enable();
|
|
|
|
const target = {
|
|
HOST_SPLASH_image: png,
|
|
};
|
|
|
|
await deviceConfig.setBootConfig(splashImageBackend, target);
|
|
|
|
expect(logSpy).to.be.calledTwice;
|
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
|
expect(
|
|
await fs.readFile(path.join(splash, 'resin-logo.png'), 'base64'),
|
|
).to.equal(png);
|
|
|
|
await tTfs.restore();
|
|
});
|
|
|
|
it('should correctly write to balena-logo.png', async () => {
|
|
// Devices with balenaOS >= 2.51 use balena-logo.png
|
|
const tTfs = await testfs({
|
|
[splash]: {
|
|
'balena-logo.png': Buffer.from(png, 'base64'),
|
|
},
|
|
}).enable();
|
|
|
|
const target = {
|
|
HOST_SPLASH_image: png,
|
|
};
|
|
|
|
await deviceConfig.setBootConfig(splashImageBackend, target);
|
|
|
|
expect(logSpy).to.be.calledTwice;
|
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
|
expect(
|
|
await fs.readFile(path.join(splash, 'balena-logo.png'), 'base64'),
|
|
).to.equal(png);
|
|
|
|
await tTfs.restore();
|
|
});
|
|
|
|
it('should correctly write to balena-logo.png if no default logo is found', async () => {
|
|
// Devices with balenaOS >= 2.51 use balena-logo.png
|
|
const current = {};
|
|
const target = {
|
|
HOST_SPLASH_image: png,
|
|
};
|
|
|
|
// This should work with every device type, but testing on a couple
|
|
// of options
|
|
expect(
|
|
deviceConfig.bootConfigChangeRequired(
|
|
splashImageBackend,
|
|
current,
|
|
target,
|
|
'raspberrypi3',
|
|
),
|
|
).to.equal(true);
|
|
|
|
await deviceConfig.setBootConfig(splashImageBackend, target);
|
|
|
|
expect(logSpy).to.be.calledTwice;
|
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
|
expect(
|
|
await fs.readFile(path.join(splash, 'balena-logo.png'), 'base64'),
|
|
).to.equal(png);
|
|
});
|
|
|
|
it('should correctly read the splash logo if different from the default', async () => {
|
|
const tTfs = await testfs({
|
|
[splash]: {
|
|
'balena-logo.png': Buffer.from(png, 'base64'),
|
|
'balena-logo-default.png': Buffer.from(defaultLogo, 'base64'),
|
|
},
|
|
}).enable();
|
|
|
|
expect(
|
|
await deviceConfig.getBootConfig(splashImageBackend),
|
|
).to.deep.equal({
|
|
HOST_SPLASH_image: uri,
|
|
});
|
|
await tTfs.restore();
|
|
});
|
|
});
|
|
|
|
describe('getRequiredSteps', () => {
|
|
const splash = pathOnBoot('splash/balena-logo.png');
|
|
|
|
const tFs = testfs({
|
|
// This is only needed so config.get doesn't fail
|
|
[configJson]: JSON.stringify({}),
|
|
[configTxt]: stripIndent`
|
|
enable_uart=true
|
|
`,
|
|
[osRelease]: stripIndent`
|
|
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
|
META_BALENA_VERSION="2.88.5"
|
|
VERSION="2.88.5+rev1"
|
|
VARIANT_ID="dev"
|
|
`,
|
|
[deviceTypeJson]: JSON.stringify({
|
|
slug: 'raspberrypi4-64',
|
|
arch: 'aarch64',
|
|
}),
|
|
[splash]: Buffer.from(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
|
'base64',
|
|
),
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await tFs.enable();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await tFs.restore();
|
|
});
|
|
|
|
it('returns required steps to config.json first if any', async () => {
|
|
const steps = await deviceConfig.getRequiredSteps(
|
|
{
|
|
local: {
|
|
config: {
|
|
SUPERVISOR_POLL_INTERVAL: 900000,
|
|
HOST_CONFIG_enable_uart: true,
|
|
},
|
|
},
|
|
} as any,
|
|
{
|
|
local: {
|
|
config: {
|
|
SUPERVISOR_POLL_INTERVAL: 600000,
|
|
HOST_CONFIG_enable_uart: false,
|
|
},
|
|
},
|
|
} as any,
|
|
);
|
|
expect(steps.map((s) => s.action)).to.have.members([
|
|
// No reboot is required by this config change
|
|
'changeConfig',
|
|
'noop', // The noop has to be here since there are also changes from config backends
|
|
]);
|
|
});
|
|
|
|
it('sets the rebooot breadcrumb for config steps that require a reboot', async () => {
|
|
const steps = await deviceConfig.getRequiredSteps(
|
|
{
|
|
local: {
|
|
config: {
|
|
SUPERVISOR_POLL_INTERVAL: 900000,
|
|
SUPERVISOR_PERSISTENT_LOGGING: false,
|
|
},
|
|
},
|
|
} as any,
|
|
{
|
|
local: {
|
|
config: {
|
|
SUPERVISOR_POLL_INTERVAL: 600000,
|
|
SUPERVISOR_PERSISTENT_LOGGING: true,
|
|
},
|
|
},
|
|
} as any,
|
|
);
|
|
expect(steps.map((s) => s.action)).to.have.members([
|
|
'setRebootBreadcrumb',
|
|
'changeConfig',
|
|
'noop',
|
|
]);
|
|
});
|
|
|
|
it('returns required steps for backends if no steps are required for config.json', async () => {
|
|
const steps = await deviceConfig.getRequiredSteps(
|
|
{
|
|
local: {
|
|
config: {
|
|
SUPERVISOR_POLL_INTERVAL: 900000,
|
|
SUPERVISOR_PERSISTENT_LOGGING: true,
|
|
HOST_CONFIG_enable_uart: true,
|
|
},
|
|
},
|
|
} as any,
|
|
{
|
|
local: {
|
|
config: {
|
|
SUPERVISOR_POLL_INTERVAL: 900000,
|
|
SUPERVISOR_PERSISTENT_LOGGING: true,
|
|
HOST_CONFIG_enable_uart: false,
|
|
},
|
|
},
|
|
} as any,
|
|
);
|
|
expect(steps.map((s) => s.action)).to.have.members([
|
|
'setRebootBreadcrumb',
|
|
'setBootConfig',
|
|
]);
|
|
});
|
|
});
|
|
});
|