mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-03-12 07:23:58 +00:00
added support for configuring ODMDATA
Closes: 1206 Change-type: minor Signed-off-by: Miguel Casqueira <miguel@balena.io>
This commit is contained in:
parent
5121aea153
commit
662826d349
@ -4,12 +4,14 @@ import { Extlinux } from './extlinux';
|
|||||||
import { ExtraUEnv } from './extra-uEnv';
|
import { ExtraUEnv } from './extra-uEnv';
|
||||||
import { ConfigTxt } from './config-txt';
|
import { ConfigTxt } from './config-txt';
|
||||||
import { ConfigFs } from './config-fs';
|
import { ConfigFs } from './config-fs';
|
||||||
|
import { Odmdata } from './odmdata';
|
||||||
|
|
||||||
export const allBackends = [
|
export const allBackends = [
|
||||||
new Extlinux(),
|
new Extlinux(),
|
||||||
new ExtraUEnv(),
|
new ExtraUEnv(),
|
||||||
new ConfigTxt(),
|
new ConfigTxt(),
|
||||||
new ConfigFs(),
|
new ConfigFs(),
|
||||||
|
new Odmdata(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export function matchesAnyBootConfig(envVar: string): boolean {
|
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 { EnvVarObject } from './lib/types';
|
||||||
import { UnitNotLoadedError } from './lib/errors';
|
import { UnitNotLoadedError } from './lib/errors';
|
||||||
import { checkInt, checkTruthy } from './lib/validation';
|
import { checkInt, checkTruthy } from './lib/validation';
|
||||||
|
import log from './lib/supervisor-console';
|
||||||
import { DeviceStatus } from './types/state';
|
import { DeviceStatus } from './types/state';
|
||||||
import * as configUtils from './config/utils';
|
import * as configUtils from './config/utils';
|
||||||
import { SchemaTypeKey } from './config/schema-type';
|
import { SchemaTypeKey } from './config/schema-type';
|
||||||
import { matchesAnyBootConfig } from './config/backends';
|
import { matchesAnyBootConfig } from './config/backends';
|
||||||
import { ConfigOptions, ConfigBackend } from './config/backends/backend';
|
import { ConfigOptions, ConfigBackend } from './config/backends/backend';
|
||||||
|
import { Odmdata } from './config/backends/odmdata';
|
||||||
|
|
||||||
const vpnServiceName = 'openvpn';
|
const vpnServiceName = 'openvpn';
|
||||||
|
|
||||||
@ -339,9 +341,8 @@ export function resetRateLimits() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exported for tests
|
|
||||||
export function bootConfigChangeRequired(
|
export function bootConfigChangeRequired(
|
||||||
configBackend: ConfigBackend | null,
|
configBackend: ConfigBackend,
|
||||||
current: Dictionary<string>,
|
current: Dictionary<string>,
|
||||||
target: Dictionary<string>,
|
target: Dictionary<string>,
|
||||||
deviceType: string,
|
deviceType: string,
|
||||||
@ -352,23 +353,39 @@ export function bootConfigChangeRequired(
|
|||||||
// Some devices require specific overlays, here we apply them
|
// Some devices require specific overlays, here we apply them
|
||||||
ensureRequiredOverlay(deviceType, targetBootConfig);
|
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)) {
|
if (!_.isEqual(currentBootConfig, targetBootConfig)) {
|
||||||
_.each(targetBootConfig, (value, key) => {
|
// Check if the only difference is the targetBootConfig not containing a special case
|
||||||
// Ignore null check because we can't get here if configBackend is null
|
const SPECIAL_CASE = 'configuration'; // ODMDATA Mode for TX2 devices
|
||||||
if (!configBackend!.isSupportedConfig(key)) {
|
if (!(SPECIAL_CASE in targetBootConfig)) {
|
||||||
if (currentBootConfig[key] !== value) {
|
// Create a copy to modify
|
||||||
const err = `Attempt to change blacklisted config value ${key}`;
|
const targetCopy = _.cloneDeep(targetBootConfig);
|
||||||
logger.logSystemMessage(
|
// Add current value to simulate if the value was set in the cloud on provision
|
||||||
err,
|
targetCopy[SPECIAL_CASE] = currentBootConfig[SPECIAL_CASE];
|
||||||
{ error: err },
|
if (_.isEqual(targetCopy, currentBootConfig)) {
|
||||||
'Apply boot config error',
|
// 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
|
||||||
throw new Error(err);
|
// 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 true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return false (no change is required)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,14 +497,14 @@ export async function getRequiredSteps(
|
|||||||
|
|
||||||
const backends = await getConfigBackends();
|
const backends = await getConfigBackends();
|
||||||
// Check for required bootConfig changes
|
// Check for required bootConfig changes
|
||||||
backends.forEach((backend) => {
|
for (const backend of backends) {
|
||||||
if (bootConfigChangeRequired(backend, current, target, deviceType)) {
|
if (changeRequired(backend, current, target, deviceType)) {
|
||||||
steps.push({
|
steps.push({
|
||||||
action: 'setBootConfig',
|
action: 'setBootConfig',
|
||||||
target,
|
target,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Check if there is either no steps, or they are all
|
// Check if there is either no steps, or they are all
|
||||||
// noops, and we need to reboot. We want to do this
|
// noops, and we need to reboot. We want to do this
|
||||||
@ -504,6 +521,43 @@ export async function getRequiredSteps(
|
|||||||
return steps;
|
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(
|
export function executeStepAction(
|
||||||
step: ConfigStep,
|
step: ConfigStep,
|
||||||
opts: DeviceActionExecutorOpts,
|
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.
|
* This can be things like missing config files or config files we cannot write to.
|
||||||
*/
|
*/
|
||||||
export class ExtraUEnvError extends TypedError {}
|
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 * as logger from '../src/logger';
|
||||||
import { Extlinux } from '../src/config/backends/extlinux';
|
import { Extlinux } from '../src/config/backends/extlinux';
|
||||||
import { ConfigTxt } from '../src/config/backends/config-txt';
|
import { ConfigTxt } from '../src/config/backends/config-txt';
|
||||||
|
import { Odmdata } from '../src/config/backends/odmdata';
|
||||||
|
|
||||||
import prepare = require('./lib/prepare');
|
import prepare = require('./lib/prepare');
|
||||||
|
|
||||||
const extlinuxBackend = new Extlinux();
|
const extlinuxBackend = new Extlinux();
|
||||||
const configTxtBackend = new ConfigTxt();
|
const configTxtBackend = new ConfigTxt();
|
||||||
|
const odmdataBackend = new Odmdata();
|
||||||
|
|
||||||
describe('Device Backend Config', () => {
|
describe('Device Backend Config', () => {
|
||||||
let logSpy: SinonSpy;
|
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', () => {
|
// describe('ConfigFS', () => {
|
||||||
// const upboardConfig = new DeviceConfig();
|
// const upboardConfig = new DeviceConfig();
|
||||||
// let upboardConfigBackend: ConfigBackend | null;
|
// 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