mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-21 02:01:35 +00:00
Merge pull request #1436 from balena-io/1206-odmdata-confugration
added support for configuring ODMDATA
This commit is contained in:
commit
0334669e1f
@ -4,12 +4,14 @@ import { Extlinux } from './extlinux';
|
||||
import { ExtraUEnv } from './extra-uEnv';
|
||||
import { ConfigTxt } from './config-txt';
|
||||
import { ConfigFs } from './config-fs';
|
||||
import { Odmdata } from './odmdata';
|
||||
|
||||
export const allBackends = [
|
||||
new Extlinux(),
|
||||
new ExtraUEnv(),
|
||||
new ConfigTxt(),
|
||||
new ConfigFs(),
|
||||
new Odmdata(),
|
||||
];
|
||||
|
||||
export function matchesAnyBootConfig(envVar: string): boolean {
|
||||
|
247
src/config/backends/odmdata.ts
Normal file
247
src/config/backends/odmdata.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import { ConfigOptions, ConfigBackend } from './backend';
|
||||
import * as constants from '../../lib/constants';
|
||||
import log from '../../lib/supervisor-console';
|
||||
import { ODMDataError } from '../../lib/errors';
|
||||
|
||||
/**
|
||||
* A backend to handle ODMDATA configuration
|
||||
*
|
||||
* Supports:
|
||||
* - {BALENA|RESIN}_HOST_CONFIG_odmdata_configuration = value | "value"
|
||||
*/
|
||||
|
||||
export class Odmdata extends ConfigBackend {
|
||||
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}ODMDATA_`;
|
||||
private static bootConfigPath = `${constants.rootMountPoint}/dev/mmcblk0boot0`;
|
||||
private static bootConfigLockPath = `${constants.rootMountPoint}/sys/block/mmcblk0boot0/force_ro`;
|
||||
private static supportedConfigs = ['configuration'];
|
||||
private BYTE_OFFSETS = [1659, 5243, 18043];
|
||||
private CONFIG_BYTES = [
|
||||
0x0 /* Config Option #1 */,
|
||||
0x1 /* Config Option #2 */,
|
||||
0x6 /* Config Option #3 */,
|
||||
0x7 /* Config Option #4 */,
|
||||
0x2 /* Config Option #5 */,
|
||||
0x3 /* Config Option #6 */,
|
||||
];
|
||||
private CONFIG_BUFFER = Buffer.from(this.CONFIG_BYTES);
|
||||
|
||||
public static bootConfigVarRegex = new RegExp(
|
||||
'(?:' + _.escapeRegExp(Odmdata.bootConfigVarPrefix) + ')(.+)',
|
||||
);
|
||||
|
||||
public async matches(deviceType: string): Promise<boolean> {
|
||||
return deviceType.endsWith('-tx2');
|
||||
}
|
||||
|
||||
public async getBootConfig(): Promise<ConfigOptions> {
|
||||
// Get config buffer from bootConfigPath
|
||||
const confBuffer = await this.readBootConfigPath();
|
||||
// Parse ConfigOptions from bootConfigPath buffer
|
||||
return this.parseOptions(confBuffer);
|
||||
}
|
||||
|
||||
public async setBootConfig(opts: ConfigOptions): Promise<void> {
|
||||
log.info('Attempting to configure ODMDATA.');
|
||||
// Filter out unsupported options
|
||||
const supportedOptions = _.pickBy(opts, (value, key) => {
|
||||
if (!this.isSupportedConfig(key)) {
|
||||
log.warn(`Not setting unsupported value: { ${key}: ${value} }`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Check that there is a configuration mode
|
||||
if (!supportedOptions.configuration) {
|
||||
log.info('No changes made to ODMDATA configuration.');
|
||||
return;
|
||||
}
|
||||
// Check that configuration mode is supported
|
||||
const mode = parseInt(supportedOptions.configuration as string, 10);
|
||||
if (mode < 1 || mode > 6) {
|
||||
log.error(`Configuration mode of: ${mode} is not supported.`);
|
||||
log.info('No changes made to ODMDATA configuration.');
|
||||
return;
|
||||
}
|
||||
log.info(`Setting ODMDATA Configuration Mode #${mode}.`);
|
||||
// bootConfigPath is a hardware partition that is READ ONLY
|
||||
// We must set bootConfigLockPath to false so we can write to this partition.
|
||||
await this.setReadOnly(false);
|
||||
try {
|
||||
const BYTE_INDEX = mode - 1;
|
||||
// Write this byte to each BYTE_OFFSETS in bootConfigPath
|
||||
for (const POSITION of this.BYTE_OFFSETS) {
|
||||
await this.setByteAtOffset(this.CONFIG_BUFFER, BYTE_INDEX, POSITION);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('Failed to set configuration mode.', e);
|
||||
throw e;
|
||||
} finally {
|
||||
// Lock RO access
|
||||
await this.setReadOnly(true);
|
||||
}
|
||||
log.info(`Successfully set ODMDATA Configuration Mode #${mode}.`);
|
||||
}
|
||||
|
||||
public isSupportedConfig(config: string): boolean {
|
||||
return Odmdata.supportedConfigs.includes(config);
|
||||
}
|
||||
|
||||
public isBootConfigVar(envVar: string): boolean {
|
||||
return envVar.startsWith(Odmdata.bootConfigVarPrefix);
|
||||
}
|
||||
|
||||
public processConfigVarName(envVar: string): string | null {
|
||||
const name = envVar.replace(Odmdata.bootConfigVarRegex, '$1');
|
||||
if (name === envVar) {
|
||||
return null;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public processConfigVarValue(_key: string, value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
public createConfigVarName(configName: string): string | null {
|
||||
if (configName === '') {
|
||||
return null;
|
||||
}
|
||||
return `${Odmdata.bootConfigVarPrefix}${configName}`;
|
||||
}
|
||||
|
||||
private parseOptions(optionsBuffer: Buffer): ConfigOptions {
|
||||
log.debug('Attempting to parse ODMDATA from Buffer.');
|
||||
// Check that all the values in the buffer match
|
||||
if (
|
||||
!(
|
||||
optionsBuffer.readUInt8(0) === optionsBuffer.readUInt8(1) &&
|
||||
optionsBuffer.readUInt8(1) === optionsBuffer.readUInt8(2)
|
||||
)
|
||||
) {
|
||||
log.error(
|
||||
'Unable to parse ODMDATA configuration. Data at offsets do not match.',
|
||||
);
|
||||
throw new ODMDataError(
|
||||
'Unable to parse ODMDATA configuration. Data at offsets do not match.',
|
||||
);
|
||||
}
|
||||
// Find the configuration given the optionsBuffer
|
||||
const configIndex = this.CONFIG_BYTES.reduce(
|
||||
(currentIndex: number, _config: number, index: number) => {
|
||||
if (
|
||||
this.CONFIG_BUFFER.readUInt8(index) === optionsBuffer.readUInt8(0)
|
||||
) {
|
||||
return index;
|
||||
}
|
||||
return currentIndex;
|
||||
},
|
||||
null,
|
||||
);
|
||||
// Check if we found a configuration we support
|
||||
if (configIndex === null) {
|
||||
log.error(
|
||||
`ODMDATA is set with an unsupported byte: 0x${optionsBuffer.readUInt8(
|
||||
0,
|
||||
)}`,
|
||||
);
|
||||
throw new ODMDataError(
|
||||
'Unable to parse ODMDATA configuration. Unsupported configuration byte set.',
|
||||
);
|
||||
}
|
||||
// Return supported configuration number currently set
|
||||
log.debug(`Parsed Configuration Mode #${configIndex + 1} for ODMDATA.`);
|
||||
return {
|
||||
configuration: `${configIndex + 1}`,
|
||||
};
|
||||
}
|
||||
|
||||
private async setByteAtOffset(
|
||||
buffer: Buffer,
|
||||
byteIndex: number,
|
||||
position: number,
|
||||
): Promise<void> {
|
||||
// Obtain a file handle on bootConfigPath
|
||||
const fileHandle = await this.getFileHandle(Odmdata.bootConfigPath);
|
||||
// Length is always 1 because this function is for only writing single byte values at a time.
|
||||
const LENGTH = 1;
|
||||
try {
|
||||
await fileHandle.write(buffer, byteIndex, LENGTH, position);
|
||||
} catch (e) {
|
||||
log.error(`Issue writing to '${Odmdata.bootConfigPath}'`, e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (fileHandle) {
|
||||
await fileHandle.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getFileHandle(
|
||||
file: string,
|
||||
flags = 'r+', // Open file for reading and writing. An exception occurs if the file does not exist.
|
||||
): Promise<fs.FileHandle> {
|
||||
try {
|
||||
return await fs.open(file, flags);
|
||||
} catch (e) {
|
||||
switch (e.code) {
|
||||
case 'ENOENT':
|
||||
log.error(`File not found at: ${file}`);
|
||||
throw new ODMDataError(`File not found at: ${file}`);
|
||||
case 'EACCES':
|
||||
log.error(`Permission denied when opening '${file}'`);
|
||||
throw new ODMDataError(`Permission denied when opening '${file}'`);
|
||||
default:
|
||||
log.error(`Unknown error when opening '${file}'`, e);
|
||||
throw new ODMDataError(`Unknown error when opening '${file}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async readBootConfigPath(): Promise<Buffer> {
|
||||
// Create a buffer to store config byte values
|
||||
const valuesBuffer = Buffer.alloc(3);
|
||||
// Obtain a file handle on bootConfigPath
|
||||
const fileHandle = await this.getFileHandle(Odmdata.bootConfigPath);
|
||||
// Set single byte values in buffer at ODMDATA offsets
|
||||
try {
|
||||
for (let offset = 0; offset < 3; offset++) {
|
||||
await fileHandle.read(
|
||||
valuesBuffer,
|
||||
offset,
|
||||
1,
|
||||
this.BYTE_OFFSETS[offset],
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(`Issue reading '${Odmdata.bootConfigPath}'`, e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (fileHandle) {
|
||||
await fileHandle.close();
|
||||
}
|
||||
}
|
||||
return valuesBuffer;
|
||||
}
|
||||
|
||||
private async setReadOnly(value: boolean): Promise<void> {
|
||||
// Normalize boolean input to binary output
|
||||
const OUTPUT = value === true ? '1' : '0';
|
||||
// Obtain a file handle on bootConfigLockPath
|
||||
const fileHandle = await this.getFileHandle(Odmdata.bootConfigLockPath);
|
||||
// Write RO flag to lock file
|
||||
try {
|
||||
await fileHandle.write(OUTPUT);
|
||||
} catch (e) {
|
||||
log.error(`Issue writing to '${Odmdata.bootConfigLockPath}'`, e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (fileHandle) {
|
||||
await fileHandle.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,11 +8,13 @@ import * as dbus from './lib/dbus';
|
||||
import { EnvVarObject } from './lib/types';
|
||||
import { UnitNotLoadedError } from './lib/errors';
|
||||
import { checkInt, checkTruthy } from './lib/validation';
|
||||
import log from './lib/supervisor-console';
|
||||
import { DeviceStatus } from './types/state';
|
||||
import * as configUtils from './config/utils';
|
||||
import { SchemaTypeKey } from './config/schema-type';
|
||||
import { matchesAnyBootConfig } from './config/backends';
|
||||
import { ConfigOptions, ConfigBackend } from './config/backends/backend';
|
||||
import { Odmdata } from './config/backends/odmdata';
|
||||
|
||||
const vpnServiceName = 'openvpn';
|
||||
|
||||
@ -339,9 +341,8 @@ export function resetRateLimits() {
|
||||
});
|
||||
}
|
||||
|
||||
// Exported for tests
|
||||
export function bootConfigChangeRequired(
|
||||
configBackend: ConfigBackend | null,
|
||||
configBackend: ConfigBackend,
|
||||
current: Dictionary<string>,
|
||||
target: Dictionary<string>,
|
||||
deviceType: string,
|
||||
@ -352,23 +353,39 @@ export function bootConfigChangeRequired(
|
||||
// Some devices require specific overlays, here we apply them
|
||||
ensureRequiredOverlay(deviceType, targetBootConfig);
|
||||
|
||||
// Search for any unsupported values
|
||||
_.each(targetBootConfig, (value, key) => {
|
||||
if (
|
||||
!configBackend.isSupportedConfig(key) &&
|
||||
currentBootConfig[key] !== value
|
||||
) {
|
||||
const err = `Attempt to change blacklisted config value ${key}`;
|
||||
logger.logSystemMessage(err, { error: err }, 'Apply boot config error');
|
||||
throw new Error(err);
|
||||
}
|
||||
});
|
||||
|
||||
if (!_.isEqual(currentBootConfig, targetBootConfig)) {
|
||||
_.each(targetBootConfig, (value, key) => {
|
||||
// Ignore null check because we can't get here if configBackend is null
|
||||
if (!configBackend!.isSupportedConfig(key)) {
|
||||
if (currentBootConfig[key] !== value) {
|
||||
const err = `Attempt to change blacklisted config value ${key}`;
|
||||
logger.logSystemMessage(
|
||||
err,
|
||||
{ error: err },
|
||||
'Apply boot config error',
|
||||
);
|
||||
throw new Error(err);
|
||||
}
|
||||
// Check if the only difference is the targetBootConfig not containing a special case
|
||||
const SPECIAL_CASE = 'configuration'; // ODMDATA Mode for TX2 devices
|
||||
if (!(SPECIAL_CASE in targetBootConfig)) {
|
||||
// Create a copy to modify
|
||||
const targetCopy = _.cloneDeep(targetBootConfig);
|
||||
// Add current value to simulate if the value was set in the cloud on provision
|
||||
targetCopy[SPECIAL_CASE] = currentBootConfig[SPECIAL_CASE];
|
||||
if (_.isEqual(targetCopy, currentBootConfig)) {
|
||||
// This proves the only difference is ODMDATA configuration is not set in target config.
|
||||
// This special case is to allow devices that upgrade to SV with ODMDATA support
|
||||
// and have no set a ODMDATA configuration in the cloud yet.
|
||||
// Normally on provision this value would have been sent to the cloud.
|
||||
return false; // (no change is required)
|
||||
}
|
||||
});
|
||||
}
|
||||
// Change is required because configs do not match
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return false (no change is required)
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -480,14 +497,14 @@ export async function getRequiredSteps(
|
||||
|
||||
const backends = await getConfigBackends();
|
||||
// Check for required bootConfig changes
|
||||
backends.forEach((backend) => {
|
||||
if (bootConfigChangeRequired(backend, current, target, deviceType)) {
|
||||
for (const backend of backends) {
|
||||
if (changeRequired(backend, current, target, deviceType)) {
|
||||
steps.push({
|
||||
action: 'setBootConfig',
|
||||
target,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if there is either no steps, or they are all
|
||||
// noops, and we need to reboot. We want to do this
|
||||
@ -504,6 +521,43 @@ export async function getRequiredSteps(
|
||||
return steps;
|
||||
}
|
||||
|
||||
function changeRequired(
|
||||
configBackend: ConfigBackend,
|
||||
currentConfig: Dictionary<string>,
|
||||
targetConfig: Dictionary<string>,
|
||||
deviceType: string,
|
||||
): boolean {
|
||||
let aChangeIsRequired = false;
|
||||
try {
|
||||
aChangeIsRequired = bootConfigChangeRequired(
|
||||
configBackend,
|
||||
currentConfig,
|
||||
targetConfig,
|
||||
deviceType,
|
||||
);
|
||||
} catch (e) {
|
||||
switch (e) {
|
||||
case 'Value missing from target configuration.':
|
||||
if (configBackend instanceof Odmdata) {
|
||||
// In this special case, devices with ODMDATA support may have
|
||||
// empty configuration options in the target if they upgraded to a SV
|
||||
// version with ODMDATA support and didn't set a value in the cloud.
|
||||
// If this is the case then we will update the cloud with the device's
|
||||
// current config and then continue without an error
|
||||
aChangeIsRequired = false;
|
||||
} else {
|
||||
log.debug(`
|
||||
The device has a configuration setting that the cloud does not have set.\nNo configurations for this backend will be set.`);
|
||||
// Set changeRequired to false so we do not get stuck in a loop trying to fix this mismatch
|
||||
aChangeIsRequired = false;
|
||||
}
|
||||
default:
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return aChangeIsRequired;
|
||||
}
|
||||
|
||||
export function executeStepAction(
|
||||
step: ConfigStep,
|
||||
opts: DeviceActionExecutorOpts,
|
||||
|
@ -133,3 +133,9 @@ export class FDTDirectiveError extends TypedError {}
|
||||
* This can be things like missing config files or config files we cannot write to.
|
||||
*/
|
||||
export class ExtraUEnvError extends TypedError {}
|
||||
|
||||
/**
|
||||
* Generic error thrown when something goes wrong with handling the ODMDATA backend.
|
||||
* This can be things like missing config files or config files we cannot write to.
|
||||
*/
|
||||
export class ODMDataError extends TypedError {}
|
||||
|
@ -8,10 +8,13 @@ import * as fsUtils from '../src/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 prepare = require('./lib/prepare');
|
||||
|
||||
const extlinuxBackend = new Extlinux();
|
||||
const configTxtBackend = new ConfigTxt();
|
||||
const odmdataBackend = new Odmdata();
|
||||
|
||||
describe('Device Backend Config', () => {
|
||||
let logSpy: SinonSpy;
|
||||
@ -310,6 +313,29 @@ describe('Device Backend Config', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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('ConfigFS', () => {
|
||||
// const upboardConfig = new DeviceConfig();
|
||||
// let upboardConfigBackend: ConfigBackend | null;
|
||||
|
276
test/34-odmdata-config.spec.ts
Normal file
276
test/34-odmdata-config.spec.ts
Normal file
@ -0,0 +1,276 @@
|
||||
import { SinonStub, stub } from 'sinon';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { expect } from './lib/chai-config';
|
||||
import Log from '../src/lib/supervisor-console';
|
||||
import { Odmdata } from '../src/config/backends/odmdata';
|
||||
|
||||
describe('ODMDATA Configuration', () => {
|
||||
const backend = new Odmdata();
|
||||
let logWarningStub: SinonStub;
|
||||
let logErrorStub: SinonStub;
|
||||
// @ts-ignore accessing private vluae
|
||||
const previousConfigPath = Odmdata.bootConfigPath;
|
||||
const testConfigPath = resolve(__dirname, 'data/boot0.img');
|
||||
|
||||
before(() => {
|
||||
// @ts-ignore setting value of private variable
|
||||
Odmdata.bootConfigPath = testConfigPath;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// @ts-ignore setting value of private variable
|
||||
Odmdata.bootConfigPath = previousConfigPath;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
logWarningStub = stub(Log, 'warn');
|
||||
logErrorStub = stub(Log, 'error');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
logWarningStub.restore();
|
||||
logErrorStub.restore();
|
||||
});
|
||||
|
||||
it('only matches supported devices', async () => {
|
||||
for (const { deviceType, match } of MATCH_TESTS) {
|
||||
await expect(backend.matches(deviceType)).to.eventually.equal(match);
|
||||
}
|
||||
});
|
||||
|
||||
it('logs error when unable to open boot config file', async () => {
|
||||
const logs = [
|
||||
{
|
||||
error: { code: 'ENOENT' },
|
||||
message: `File not found at: ${testConfigPath}`,
|
||||
},
|
||||
{
|
||||
error: { code: 'EACCES' },
|
||||
message: `Permission denied when opening '${testConfigPath}'`,
|
||||
},
|
||||
{
|
||||
error: { code: 'UNKNOWN ISSUE' }, // not a real code
|
||||
message: `Unknown error when opening '${testConfigPath}'`,
|
||||
},
|
||||
];
|
||||
const openFileStub = stub(fs, 'open');
|
||||
for (const log of logs) {
|
||||
// Stub openFileStub with specific error
|
||||
openFileStub.rejects(log.error);
|
||||
try {
|
||||
// @ts-ignore accessing private value
|
||||
await backend.getFileHandle(testConfigPath);
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
// Check that correct message was logged
|
||||
expect(logErrorStub.lastCall?.args[0]).to.equal(log.message);
|
||||
}
|
||||
openFileStub.restore();
|
||||
});
|
||||
|
||||
it('should parse configuration options from bootConfigPath', async () => {
|
||||
// Restore openFile so test actually uses testConfigPath
|
||||
await expect(backend.getBootConfig()).to.eventually.deep.equal({
|
||||
configuration: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly parses configuration mode', async () => {
|
||||
for (const config of CONFIG_MODES) {
|
||||
// @ts-ignore accessing private value
|
||||
expect(backend.parseOptions(config.buffer)).to.deep.equal({
|
||||
configuration: config.mode,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('logs error for malformed configuration mode', async () => {
|
||||
// Logs when configuration mode is unknown
|
||||
try {
|
||||
// @ts-ignore accessing private value
|
||||
backend.parseOptions(Buffer.from([0x9, 0x9, 0x9]));
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
// Check that correct message was logged
|
||||
expect(logErrorStub.lastCall?.lastArg).to.equal(
|
||||
'ODMDATA is set with an unsupported byte: 0x9',
|
||||
);
|
||||
|
||||
// Logs when bytes don't match
|
||||
try {
|
||||
// @ts-ignore accessing private value
|
||||
backend.parseOptions(Buffer.from([0x1, 0x0, 0x0]));
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
// Check that correct message was logged
|
||||
expect(logErrorStub.lastCall?.lastArg).to.equal(
|
||||
'Unable to parse ODMDATA configuration. Data at offsets do not match.',
|
||||
);
|
||||
});
|
||||
|
||||
it('unlock/lock bootConfigPath RO access', async () => {
|
||||
const writeSpy = stub().resolves();
|
||||
// @ts-ignore accessing private value
|
||||
const handleStub = stub(backend, 'getFileHandle').resolves({
|
||||
write: writeSpy,
|
||||
close: async (): Promise<void> => {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore accessing private value
|
||||
await backend.setReadOnly(false); // Try to unlock
|
||||
expect(writeSpy).to.be.calledWith('0');
|
||||
|
||||
// @ts-ignore accessing private value
|
||||
await backend.setReadOnly(true); // Try to lock
|
||||
expect(writeSpy).to.be.calledWith('1');
|
||||
|
||||
handleStub.restore();
|
||||
});
|
||||
|
||||
it('sets new config values', async () => {
|
||||
// @ts-ignore accessing private value
|
||||
const setROStub = stub(backend, 'setReadOnly');
|
||||
setROStub.resolves();
|
||||
// Get current config
|
||||
const originalConfig = await backend.getBootConfig();
|
||||
try {
|
||||
// Sets a new configuration
|
||||
await backend.setBootConfig({
|
||||
configuration: '4',
|
||||
});
|
||||
// Check that new configuration was set correctly
|
||||
await expect(backend.getBootConfig()).to.eventually.deep.equal({
|
||||
configuration: '4',
|
||||
});
|
||||
} finally {
|
||||
// Restore previous value
|
||||
await backend.setBootConfig(originalConfig);
|
||||
setROStub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('only allows supported configuration modes', () => {
|
||||
[
|
||||
{ configName: 'configuration', supported: true },
|
||||
{ configName: 'mode', supported: false },
|
||||
{ configName: '', supported: false },
|
||||
].forEach(({ configName, supported }) =>
|
||||
expect(backend.isSupportedConfig(configName)).to.equal(supported),
|
||||
);
|
||||
});
|
||||
|
||||
it('correctly detects boot config variables', () => {
|
||||
[
|
||||
{ config: 'HOST_ODMDATA_configuration', valid: true },
|
||||
{ config: 'ODMDATA_configuration', valid: false },
|
||||
{ config: 'HOST_CONFIG_odmdata_configuration', valid: false },
|
||||
{ config: 'HOST_EXTLINUX_rootwait', valid: false },
|
||||
{ config: '', valid: false },
|
||||
].forEach(({ config, valid }) =>
|
||||
expect(backend.isBootConfigVar(config)).to.equal(valid),
|
||||
);
|
||||
});
|
||||
|
||||
it('converts variable to backend formatted name', () => {
|
||||
[
|
||||
{ input: 'HOST_ODMDATA_configuration', output: 'configuration' },
|
||||
{ input: 'HOST_ODMDATA_', output: null },
|
||||
{ input: 'value', output: null },
|
||||
].forEach(({ input, output }) =>
|
||||
expect(backend.processConfigVarName(input)).to.equal(output),
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes variable value', () => {
|
||||
[
|
||||
{ input: { key: 'key', value: 'value' }, output: 'value' },
|
||||
].forEach(({ input, output }) =>
|
||||
expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
|
||||
output,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the environment name for config variable', () => {
|
||||
[
|
||||
{ input: 'configuration', output: 'HOST_ODMDATA_configuration' },
|
||||
{ input: '', output: null },
|
||||
].forEach(({ input, output }) =>
|
||||
expect(backend.createConfigVarName(input)).to.equal(output),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const CONFIG_MODES = [
|
||||
{
|
||||
mode: '1',
|
||||
buffer: Buffer.from([0x0, 0x0, 0x0]),
|
||||
},
|
||||
{
|
||||
mode: '2',
|
||||
buffer: Buffer.from([0x1, 0x1, 0x1]),
|
||||
},
|
||||
{
|
||||
mode: '3',
|
||||
buffer: Buffer.from([0x6, 0x6, 0x6]),
|
||||
},
|
||||
{
|
||||
mode: '4',
|
||||
buffer: Buffer.from([0x7, 0x7, 0x7]),
|
||||
},
|
||||
{
|
||||
mode: '5',
|
||||
buffer: Buffer.from([0x2, 0x2, 0x2]),
|
||||
},
|
||||
{
|
||||
mode: '6',
|
||||
buffer: Buffer.from([0x3, 0x3, 0x3]),
|
||||
},
|
||||
];
|
||||
|
||||
const MATCH_TESTS = [
|
||||
{
|
||||
deviceType: 'blackboard-tx2',
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
deviceType: 'jetson-tx2',
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
deviceType: 'n510-tx2',
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
deviceType: 'orbitty-tx2',
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
deviceType: 'spacely-tx2',
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
deviceType: 'srd3-tx2',
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
deviceType: 'raspberry-pi',
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
deviceType: 'up-board',
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
deviceType: '',
|
||||
match: false,
|
||||
},
|
||||
];
|
BIN
test/data/boot0.img
Normal file
BIN
test/data/boot0.img
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user