Added support for configuring FDT directive in extlinux.conf

Change-type: minor
Signed-off-by: Miguel Casqueira <miguel@balena.io>
This commit is contained in:
Miguel Casqueira 2020-06-16 16:05:42 -04:00
parent 3098abeca5
commit 59fc589eb2
12 changed files with 680 additions and 304 deletions

View File

@ -1,15 +1,15 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import { child_process } from 'mz'; import { child_process } from 'mz';
import * as constants from '../lib/constants'; import * as constants from '../../lib/constants';
import { writeFileAtomic } from '../lib/fs-utils'; import { writeFileAtomic } from '../../lib/fs-utils';
export const bootMountPoint = `${constants.rootMountPoint}${constants.bootMountPoint}`;
export interface ConfigOptions { export interface ConfigOptions {
[key: string]: string | string[]; [key: string]: string | string[];
} }
export const bootMountPoint = `${constants.rootMountPoint}${constants.bootMountPoint}`;
export async function remountAndWriteAtomic( export async function remountAndWriteAtomic(
file: string, file: string,
data: string, data: string,

View File

@ -7,13 +7,13 @@ import {
DeviceConfigBackend, DeviceConfigBackend,
bootMountPoint, bootMountPoint,
remountAndWriteAtomic, remountAndWriteAtomic,
} from '../backend'; } from './backend';
import * as constants from '../../lib/constants'; import * as constants from '../../lib/constants';
import * as logger from '../../logger'; import * as logger from '../../logger';
import log from '../../lib/supervisor-console'; import log from '../../lib/supervisor-console';
/** /**
* A backend to handle ConfigFS host configuration for ACPI SSDT loading * A backend to handle ConfigFS host configuration
* *
* Supports: * Supports:
* - {BALENA|RESIN}_HOST_CONFIGFS_ssdt = value | "value" | "value1","value2" * - {BALENA|RESIN}_HOST_CONFIGFS_ssdt = value | "value" | "value1","value2"
@ -94,7 +94,7 @@ export class ConfigfsConfigBackend extends DeviceConfigBackend {
`AML: ${oemId.trim()} ${oemTableId.trim()} (Rev ${oemRevision.trim()})`, `AML: ${oemId.trim()} ${oemTableId.trim()} (Rev ${oemRevision.trim()})`,
); );
} catch (e) { } catch (e) {
log.error(e); log.error('Issue while loading AML ${aml}', e);
} }
return true; return true;
} }
@ -121,7 +121,7 @@ export class ConfigfsConfigBackend extends DeviceConfigBackend {
} }
private async loadConfiguredSsdt(config: ConfigfsConfig): Promise<void> { private async loadConfiguredSsdt(config: ConfigfsConfig): Promise<void> {
if (_.isArray(config['ssdt'])) { if (Array.isArray(config['ssdt'])) {
log.info('Loading configured SSDTs'); log.info('Loading configured SSDTs');
for (const aml of config['ssdt']) { for (const aml of config['ssdt']) {
await this.loadAML(aml); await this.loadAML(aml);
@ -169,7 +169,7 @@ export class ConfigfsConfigBackend extends DeviceConfigBackend {
// see which SSDTs we have configured... // see which SSDTs we have configured...
const ssdt = config['ssdt']; const ssdt = config['ssdt'];
if (_.isArray(ssdt) && ssdt.length > 0) { if (Array.isArray(ssdt) && ssdt.length > 0) {
// we have some... // we have some...
options['ssdt'] = ssdt; options['ssdt'] = ssdt;
} }

View File

@ -0,0 +1,166 @@
import * as _ from 'lodash';
import { ConfigOptions } from './backend';
import {
ExtLinuxParseError,
AppendDirectiveError,
FDTDirectiveError,
} from '../../lib/errors';
export interface ExtlinuxFile {
globals: Directive;
labels: Label;
}
export interface Label {
[labelName: string]: Directive;
}
export interface Directive {
[directive: string]: string;
}
/**
* ConfigurableDirective
*
* This class abstraction is the blueprint used to create new directives in extlinux
* that we would want to be able to parse (get the value) and generate (create a value).
*
*/
export abstract class ConfigurableDirective {
// Parses the values for this directive
public abstract parse(directives: Directive): ConfigOptions;
// Return the value to be set for this directive using the provided ConfigOptions
public abstract generate(opts: ConfigOptions, existingValue?: string): string;
}
/**
* AppendDirective
*
* Add one or more options to the kernel command line.
*
*/
export class AppendDirective extends ConfigurableDirective {
private supportedConfigValues: string[];
public constructor(supportedConfigValues: string[]) {
super();
this.supportedConfigValues = supportedConfigValues;
}
/**
* Parses a APPEND directive string into a ConfigOptions
*
* Example:
* parse({ APPEND: "ro rootwait isolcpus=0,4" })
* -> { 'ro': '', 'rootwait': '', 'isolcpus': '0,4' }
*
*/
public parse(directives: Directive): ConfigOptions {
// Check that there is an APPEND directive to parse
if (directives.APPEND == null) {
throw new ExtLinuxParseError(
'Could not find APPEND directive in default extlinux.conf boot entry',
);
}
// Parse all the key and values into ConfigOptions
return directives.APPEND.split(' ').reduce(
(configOpts: ConfigOptions, appendValue: string) => {
// Break this append config into key and value
const [KEY, VALUE = '', more] = appendValue.split('=', 3);
if (!KEY) {
return configOpts; // No value to set so return
} else if (more != null) {
// APPEND value is not formatted correctly
// Example: isolcpus=3=2 (more then 1 value being set)
throw new AppendDirectiveError(
`Unable to parse invalid value: ${appendValue}`,
);
}
// Return key value pair with existing configs
return { [KEY]: VALUE, ...configOpts };
},
{},
);
}
/**
* Generates a string value for APPEND directive given a ConfigOptions
*
* Keys in existingValue that are also in the provided ConfigOptions
* will be replaced with those from opts.
*
* Example:
* generate({ isolcpus: '0,4' })
* -> 'isolcpus=0,4'
*
*/
public generate(opts: ConfigOptions, existingValue: string = ''): string {
// Parse current append line and remove whitelisted values
// We remove whitelisted values to avoid duplicates
const appendLine = existingValue.split(' ').filter((entry) => {
const lhs = entry.split('=', 1);
return !this.supportedConfigValues.includes(lhs[0]);
});
// Add new configurations values to the provided append line
return appendLine
.concat(
_.map(opts, (value, key) => {
if (key.includes('=') || value.includes('=')) {
throw new AppendDirectiveError(
`One of the values being set contains an invalid character: [ value: ${value}, key: ${key} ]`,
);
} else if (!value) {
// Example: rootwait (config without a value)
return `${key}`;
} else {
// Example: isolcpus=2,3 (config with a value)
return `${key}=${value}`;
}
}),
)
.join(' ')
.trim();
}
}
/**
* FDTDirective
*
* Configure the location of Device Tree Binary
*
*/
export class FDTDirective extends ConfigurableDirective {
/**
* Parses a FDT directive string into a ConfigOptions
*
* Example:
* parse({ FDT: '/boot/mycustomdtb.dtb' })
* -> { 'fdt': '/boot/mycustomdtb.dtb' }
*
*/
public parse(directives: Directive): ConfigOptions {
// NOTE: We normalize FDT to lowercase fdt
return directives.FDT ? { fdt: directives.FDT } : {};
}
/**
* Generates a string value for FDT directive given a ConfigOptions
*
* Example:
* generate({ fdt: '/boot/mycustomdtb.dtb' })
* -> '/boot/mycustomdtb.dtb'
*
*/
public generate(opts: ConfigOptions): string {
if (typeof opts.fdt !== 'string') {
throw new FDTDirectiveError(
`Cannot set FDT of non-string value: ${opts.fdt}`,
);
}
if (opts.fdt.length === 0) {
throw new FDTDirectiveError('Cannot set FDT of an empty value.');
}
return opts.fdt;
}
}

View File

@ -6,38 +6,45 @@ import {
DeviceConfigBackend, DeviceConfigBackend,
bootMountPoint, bootMountPoint,
remountAndWriteAtomic, remountAndWriteAtomic,
} from '../backend'; } from './backend';
import {
ExtlinuxFile,
Directive,
AppendDirective,
FDTDirective,
} from './extlinux-file';
import * as constants from '../../lib/constants'; import * as constants from '../../lib/constants';
import log from '../../lib/supervisor-console'; import log from '../../lib/supervisor-console';
import { ExtLinuxParseError } from '../../lib/errors';
/** /**
* A backend to handle ConfigFS host configuration for ACPI SSDT loading * A backend to handle extlinux host configuration
* *
* Supports: * Supports:
* - {BALENA|RESIN}_HOST_CONFIGFS_ssdt = value | "value" | "value1","value2" * - {BALENA|RESIN}_HOST_EXTLINUX_isolcpus = value | "value" | "value1","value2"
* - {BALENA|RESIN}_HOST_EXTLINUX_fdt = value | "value"
*/ */
interface ExtlinuxFile {
labels: {
[labelName: string]: {
[directive: string]: string;
};
};
globals: { [directive: string]: string };
}
export class ExtlinuxConfigBackend extends DeviceConfigBackend { export class ExtlinuxConfigBackend extends DeviceConfigBackend {
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}EXTLINUX_`; private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}EXTLINUX_`;
private static bootConfigPath = `${bootMountPoint}/extlinux/extlinux.conf`; private static bootConfigPath = `${bootMountPoint}/extlinux/extlinux.conf`;
private static supportedConfigValues = ['isolcpus', 'fdt'];
private static supportedDirectives = ['APPEND', 'FDT'];
public static bootConfigVarRegex = new RegExp( private fdtDirective = new FDTDirective();
'(' + _.escapeRegExp(ExtlinuxConfigBackend.bootConfigVarPrefix) + ')(.+)', private appendDirective = new AppendDirective(
// Pass in list of supportedConfigValues that APPEND can have
ExtlinuxConfigBackend.supportedConfigValues.filter(
(v) => !this.isDirective(v),
),
); );
private static supportedConfigKeys = ['isolcpus']; public static bootConfigVarRegex = new RegExp(
'(?:' + _.escapeRegExp(ExtlinuxConfigBackend.bootConfigVarPrefix) + ')(.+)',
);
public matches(deviceType: string): boolean { public matches(deviceType: string): boolean {
return _.startsWith(deviceType, 'jetson-tx'); return deviceType.startsWith('jetson-tx');
} }
public async getBootConfig(): Promise<ConfigOptions> { public async getBootConfig(): Promise<ConfigOptions> {
@ -51,59 +58,38 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
} catch { } catch {
// In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot // In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot
// We do not have any backup to fallback too; warn the user of a possible brick // We do not have any backup to fallback too; warn the user of a possible brick
throw new Error( throw new ExtLinuxParseError(
'Could not find extlinux file. Device is possibly bricked', 'Could not find extlinux file. Device is possibly bricked',
); );
} }
// Parse ExtlinuxFile from file contents
const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile( const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile(
confContents, confContents,
); );
// First find the default label name // Get default label to know which label entry to parse
const defaultLabel = _.find(parsedBootFile.globals, (_v, l) => { const defaultLabel = ExtlinuxConfigBackend.findDefaultLabel(parsedBootFile);
if (l === 'DEFAULT') {
return true;
}
return false;
});
if (defaultLabel == null) { // Get the label entry we will parse
throw new Error('Could not find default entry for extlinux.conf file'); const labelEntry = ExtlinuxConfigBackend.getLabelEntry(
} parsedBootFile,
defaultLabel,
);
const labelEntry = parsedBootFile.labels[defaultLabel]; // Parse APPEND directive and filter out unsupported values
const appendConfig = _.pickBy(
this.appendDirective.parse(labelEntry),
(_value, key) => this.isSupportedConfig(key),
);
if (labelEntry == null) { // Parse FDT directive
throw new Error( const fdtConfig = this.fdtDirective.parse(labelEntry);
`Cannot find default label entry (label: ${defaultLabel}) for extlinux.conf file`,
);
}
// All configuration options come from the `APPEND` directive in the default label entry return {
const appendEntry = labelEntry.APPEND; ...appendConfig,
...fdtConfig,
if (appendEntry == null) { };
throw new Error(
'Could not find APPEND directive in default extlinux.conf boot entry',
);
}
const conf: ConfigOptions = {};
const values = appendEntry.split(' ');
for (const value of values) {
const parts = value.split('=');
if (this.isSupportedConfig(parts[0])) {
if (parts.length !== 2) {
throw new Error(
`Could not parse extlinux configuration entry: ${values} [value with error: ${value}]`,
);
}
conf[parts[0]] = parts[1];
}
}
return conf;
} }
public async setBootConfig(opts: ConfigOptions): Promise<void> { public async setBootConfig(opts: ConfigOptions): Promise<void> {
@ -123,60 +109,55 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
); );
} }
const extlinuxFile = ExtlinuxConfigBackend.parseExtlinuxFile( // Parse ExtlinuxFile from file contents
confContents.toString(), const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile(
); confContents,
const defaultLabel = extlinuxFile.globals.DEFAULT;
if (defaultLabel == null) {
throw new Error(
'Could not find DEFAULT directive entry in extlinux.conf',
);
}
const defaultEntry = extlinuxFile.labels[defaultLabel];
if (defaultEntry == null) {
throw new Error(
`Could not find default extlinux.conf entry: ${defaultLabel}`,
);
}
if (defaultEntry.APPEND == null) {
throw new Error(
`extlinux.conf APPEND directive not found for default entry: ${defaultLabel}, not sure how to proceed!`,
);
}
const appendLine = _.filter(defaultEntry.APPEND.split(' '), (entry) => {
const lhs = entry.split('=');
return !this.isSupportedConfig(lhs[0]);
});
// Apply the new configuration to the "plain" append line above
_.each(opts, (value, key) => {
appendLine.push(`${key}=${value}`);
});
defaultEntry.APPEND = appendLine.join(' ');
const extlinuxString = ExtlinuxConfigBackend.extlinuxFileToString(
extlinuxFile,
); );
await remountAndWriteAtomic( // Get default label to know which label entry to edit
const defaultLabel = ExtlinuxConfigBackend.findDefaultLabel(parsedBootFile);
// Get the label entry we will edit
const defaultEntry = ExtlinuxConfigBackend.getLabelEntry(
parsedBootFile,
defaultLabel,
);
// Set `FDT` directive if a value is provided
if (opts.fdt) {
defaultEntry.FDT = this.fdtDirective.generate(opts);
}
// Remove unsupported options
const appendOptions = _.pickBy(
opts,
// supportedConfigValues has values AND directives so we must filter directives out
(_value, key) => this.isSupportedConfig(key) && !this.isDirective(key),
);
// Add config values to `APPEND` directive
defaultEntry.APPEND = this.appendDirective.generate(
appendOptions,
defaultEntry.APPEND,
);
// Write new extlinux configuration
return await remountAndWriteAtomic(
ExtlinuxConfigBackend.bootConfigPath, ExtlinuxConfigBackend.bootConfigPath,
extlinuxString, ExtlinuxConfigBackend.extlinuxFileToString(parsedBootFile),
); );
} }
public isSupportedConfig(configName: string): boolean { public isSupportedConfig(configName: string): boolean {
return _.includes(ExtlinuxConfigBackend.supportedConfigKeys, configName); return ExtlinuxConfigBackend.supportedConfigValues.includes(configName);
} }
public isBootConfigVar(envVar: string): boolean { public isBootConfigVar(envVar: string): boolean {
return _.startsWith(envVar, ExtlinuxConfigBackend.bootConfigVarPrefix); return envVar.startsWith(ExtlinuxConfigBackend.bootConfigVarPrefix);
} }
public processConfigVarName(envVar: string): string { public processConfigVarName(envVar: string): string {
return envVar.replace(ExtlinuxConfigBackend.bootConfigVarRegex, '$2'); return envVar.replace(ExtlinuxConfigBackend.bootConfigVarRegex, '$1');
} }
public processConfigVarValue(_key: string, value: string): string { public processConfigVarValue(_key: string, value: string): string {
@ -187,19 +168,20 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
return `${ExtlinuxConfigBackend.bootConfigVarPrefix}${configName}`; return `${ExtlinuxConfigBackend.bootConfigVarPrefix}${configName}`;
} }
private isDirective(configName: string): boolean {
return ExtlinuxConfigBackend.supportedDirectives.includes(
configName.toUpperCase(),
);
}
private static parseExtlinuxFile(confStr: string): ExtlinuxFile { private static parseExtlinuxFile(confStr: string): ExtlinuxFile {
const file: ExtlinuxFile = { const file: ExtlinuxFile = {
globals: {}, globals: {},
labels: {}, labels: {},
}; };
// Firstly split by line and filter any comments and empty lines // Split by line and filter any comments and empty lines
let lines = confStr.split(/\r?\n/); const lines = confStr.split(/(?:\r?\n[\s#]*)+/);
lines = _.filter(lines, (l) => {
const trimmed = _.trimStart(l);
return trimmed !== '' && !_.startsWith(trimmed, '#');
});
let lastLabel = ''; let lastLabel = '';
for (const line of lines) { for (const line of lines) {
@ -248,4 +230,23 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
}); });
return ret; return ret;
} }
private static findDefaultLabel(file: ExtlinuxFile): string {
if (!file.globals.DEFAULT) {
throw new ExtLinuxParseError(
'Could not find default entry for extlinux.conf file',
);
}
return file.globals.DEFAULT;
}
private static getLabelEntry(file: ExtlinuxFile, label: string): Directive {
const labelEntry = file.labels[label];
if (labelEntry == null) {
throw new ExtLinuxParseError(
`Cannot find label entry (label: ${label}) for extlinux.conf file`,
);
}
return labelEntry;
}
} }

View File

@ -6,15 +6,19 @@ import {
DeviceConfigBackend, DeviceConfigBackend,
bootMountPoint, bootMountPoint,
remountAndWriteAtomic, remountAndWriteAtomic,
} from '../backend'; } from './backend';
import * as constants from '../../lib/constants'; import * as constants from '../../lib/constants';
import log from '../../lib/supervisor-console'; import log from '../../lib/supervisor-console';
/** /**
* A backend to handle ConfigFS host configuration for ACPI SSDT loading * A backend to handle Raspberry Pi host configuration
* *
* Supports: * Supports:
* - {BALENA|RESIN}_HOST_CONFIGFS_ssdt = value | "value" | "value1","value2" * - {BALENA|RESIN}_HOST_CONFIG_dtparam = value | "value" | "value1","value2"
* - {BALENA|RESIN}_HOST_CONFIG_dtoverlay = value | "value" | "value1","value2"
* - {BALENA|RESIN}_HOST_CONFIG_device_tree_param = value | "value" | "value1","value2"
* - {BALENA|RESIN}_HOST_CONFIG_device_tree_overlay = value | "value" | "value1","value2"
* - {BALENA|RESIN}_HOST_CONFIG_gpio = value | "value" | "value1","value2"
*/ */
export class RPiConfigBackend extends DeviceConfigBackend { export class RPiConfigBackend extends DeviceConfigBackend {
@ -22,7 +26,7 @@ export class RPiConfigBackend extends DeviceConfigBackend {
private static bootConfigPath = `${bootMountPoint}/config.txt`; private static bootConfigPath = `${bootMountPoint}/config.txt`;
public static bootConfigVarRegex = new RegExp( public static bootConfigVarRegex = new RegExp(
'(' + _.escapeRegExp(RPiConfigBackend.bootConfigVarPrefix) + ')(.+)', '(?:' + _.escapeRegExp(RPiConfigBackend.bootConfigVarPrefix) + ')(.+)',
); );
private static arrayConfigKeys = [ private static arrayConfigKeys = [
@ -47,7 +51,7 @@ export class RPiConfigBackend extends DeviceConfigBackend {
]; ];
public matches(deviceType: string): boolean { public matches(deviceType: string): boolean {
return _.startsWith(deviceType, 'raspberry') || deviceType === 'fincm3'; return deviceType.startsWith('raspberry') || deviceType === 'fincm3';
} }
public async getBootConfig(): Promise<ConfigOptions> { public async getBootConfig(): Promise<ConfigOptions> {
@ -68,20 +72,20 @@ export class RPiConfigBackend extends DeviceConfigBackend {
for (const configStr of configStatements) { for (const configStr of configStatements) {
// Don't show warnings for comments and empty lines // Don't show warnings for comments and empty lines
const trimmed = _.trimStart(configStr); const trimmed = _.trimStart(configStr);
if (_.startsWith(trimmed, '#') || trimmed === '') { if (trimmed.startsWith('#') || trimmed === '') {
continue; continue;
} }
let keyValue = /^([^=]+)=(.*)$/.exec(configStr); let keyValue = /^([^=]+)=(.*)$/.exec(configStr);
if (keyValue != null) { if (keyValue != null) {
const [, key, value] = keyValue; const [, key, value] = keyValue;
if (!_.includes(RPiConfigBackend.arrayConfigKeys, key)) { if (!RPiConfigBackend.arrayConfigKeys.includes(key)) {
conf[key] = value; conf[key] = value;
} else { } else {
if (conf[key] == null) { if (conf[key] == null) {
conf[key] = []; conf[key] = [];
} }
const confArr = conf[key]; const confArr = conf[key];
if (!_.isArray(confArr)) { if (!Array.isArray(confArr)) {
throw new Error( throw new Error(
`Expected '${key}' to have a config array but got ${typeof confArr}`, `Expected '${key}' to have a config array but got ${typeof confArr}`,
); );
@ -105,40 +109,34 @@ export class RPiConfigBackend extends DeviceConfigBackend {
} }
public async setBootConfig(opts: ConfigOptions): Promise<void> { public async setBootConfig(opts: ConfigOptions): Promise<void> {
let confStatements: string[] = []; const confStatements = _.flatMap(opts, (value, key) => {
_.each(opts, (value, key) => {
if (key === 'initramfs') { if (key === 'initramfs') {
confStatements.push(`${key} ${value}`); return `${key} ${value}`;
} else if (_.isArray(value)) { } else if (Array.isArray(value)) {
confStatements = confStatements.concat( return value.map((entry) => `${key}=${entry}`);
_.map(value, (entry) => `${key}=${entry}`),
);
} else { } else {
confStatements.push(`${key}=${value}`); return `${key}=${value}`;
} }
}); });
const confStr = `${confStatements.join('\n')}\n`; const confStr = `${confStatements.join('\n')}\n`;
await remountAndWriteAtomic(RPiConfigBackend.bootConfigPath, confStr); await remountAndWriteAtomic(RPiConfigBackend.bootConfigPath, confStr);
} }
public isSupportedConfig(configName: string): boolean { public isSupportedConfig(configName: string): boolean {
return !_.includes(RPiConfigBackend.forbiddenConfigKeys, configName); return !RPiConfigBackend.forbiddenConfigKeys.includes(configName);
} }
public isBootConfigVar(envVar: string): boolean { public isBootConfigVar(envVar: string): boolean {
return _.startsWith(envVar, RPiConfigBackend.bootConfigVarPrefix); return envVar.startsWith(RPiConfigBackend.bootConfigVarPrefix);
} }
public processConfigVarName(envVar: string): string { public processConfigVarName(envVar: string): string {
return envVar.replace(RPiConfigBackend.bootConfigVarRegex, '$2'); return envVar.replace(RPiConfigBackend.bootConfigVarRegex, '$1');
} }
public processConfigVarValue(key: string, value: string): string | string[] { public processConfigVarValue(key: string, value: string): string | string[] {
if (_.includes(RPiConfigBackend.arrayConfigKeys, key)) { if (RPiConfigBackend.arrayConfigKeys.includes(key)) {
if (!_.startsWith(value, '"')) { if (!value.startsWith('"')) {
return [value]; return [value];
} else { } else {
return JSON.parse(`[${value}]`); return JSON.parse(`[${value}]`);

View File

@ -4,7 +4,7 @@ import { EnvVarObject } from '../lib/types';
import { ExtlinuxConfigBackend } from './backends/extlinux'; import { ExtlinuxConfigBackend } from './backends/extlinux';
import { RPiConfigBackend } from './backends/raspberry-pi'; import { RPiConfigBackend } from './backends/raspberry-pi';
import { ConfigfsConfigBackend } from './backends/config-fs'; import { ConfigfsConfigBackend } from './backends/config-fs';
import { ConfigOptions, DeviceConfigBackend } from './backend'; import { ConfigOptions, DeviceConfigBackend } from './backends/backend';
const configBackends = [ const configBackends = [
new ExtlinuxConfigBackend(), new ExtlinuxConfigBackend(),

View File

@ -6,7 +6,7 @@ import { SchemaTypeKey } from './config/schema-type';
import * as db from './db'; import * as db from './db';
import * as logger from './logger'; import * as logger from './logger';
import { ConfigOptions, DeviceConfigBackend } from './config/backend'; import { ConfigOptions, DeviceConfigBackend } from './config/backends/backend';
import * as configUtils from './config/utils'; import * as configUtils from './config/utils';
import * as dbus from './lib/dbus'; import * as dbus from './lib/dbus';
import { UnitNotLoadedError } from './lib/errors'; import { UnitNotLoadedError } from './lib/errors';

View File

@ -106,3 +106,7 @@ export class ContractViolationError extends TypedError {
export class AppsJsonParseError extends TypedError {} export class AppsJsonParseError extends TypedError {}
export class DatabaseParseError extends TypedError {} export class DatabaseParseError extends TypedError {}
export class BackupError extends TypedError {} export class BackupError extends TypedError {}
export class ExtLinuxParseError extends TypedError {}
export class AppendDirectiveError extends TypedError {}
export class FDTDirectiveError extends TypedError {}

View File

@ -10,7 +10,7 @@ import * as fsUtils from '../src/lib/fs-utils';
import * as logger from '../src/logger'; import * as logger from '../src/logger';
import { ExtlinuxConfigBackend } from '../src/config/backends/extlinux'; import { ExtlinuxConfigBackend } from '../src/config/backends/extlinux';
import { RPiConfigBackend } from '../src/config/backends/raspberry-pi'; import { RPiConfigBackend } from '../src/config/backends/raspberry-pi';
import { DeviceConfigBackend } from '../src/config/backend'; import { DeviceConfigBackend } from '../src/config/backends/backend';
import prepare = require('./lib/prepare'); import prepare = require('./lib/prepare');
const extlinuxBackend = new ExtlinuxConfigBackend(); const extlinuxBackend = new ExtlinuxConfigBackend();
@ -48,12 +48,12 @@ describe('Device Backend Config', () => {
// Stub readFile to return a config that has initramfs and array variables // Stub readFile to return a config that has initramfs and array variables
stub(fs, 'readFile').resolves(stripIndent` stub(fs, 'readFile').resolves(stripIndent`
initramfs initramf.gz 0x00800000\n\ initramfs initramf.gz 0x00800000
dtparam=i2c=on\n\ dtparam=i2c=on
dtparam=audio=on\n\ dtparam=audio=on
dtoverlay=ads7846\n\ dtoverlay=ads7846
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13\n\ dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13
foobar=baz\n\ foobar=baz
`); `);
await expect( await expect(
@ -154,13 +154,13 @@ describe('Device Backend Config', () => {
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success'); expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(fsUtils.writeFileAtomic).to.be.calledWith( expect(fsUtils.writeFileAtomic).to.be.calledWith(
'./test/data/mnt/boot/config.txt', './test/data/mnt/boot/config.txt',
stripIndent`\ stripIndent`
initramfs initramf.gz 0x00800000\n\ initramfs initramf.gz 0x00800000
dtparam=i2c=on\n\ dtparam=i2c=on
dtparam=audio=off\n\ dtparam=audio=off
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13\n\ dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13
foobar=bat\n\ foobar=bat
foobaz=bar\n\ foobaz=bar
` + '\n', // add newline because stripIndent trims last newline ` + '\n', // add newline because stripIndent trims last newline
); );
@ -209,106 +209,6 @@ describe('Device Backend Config', () => {
}); });
describe('Extlinux files', () => { describe('Extlinux files', () => {
it('should parse a extlinux.conf file', () => {
const text = stripIndent`\
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait\
`;
// @ts-ignore accessing private method
const parsed = ExtlinuxConfigBackend.parseExtlinuxFile(text);
expect(parsed.globals).to.have.property('DEFAULT').that.equals('primary');
expect(parsed.globals).to.have.property('TIMEOUT').that.equals('30');
expect(parsed.globals)
.to.have.property('MENU TITLE')
.that.equals('Boot Options');
expect(parsed.labels).to.have.property('primary');
const { primary } = parsed.labels;
expect(primary)
.to.have.property('MENU LABEL')
.that.equals('primary Image');
expect(primary).to.have.property('LINUX').that.equals('/Image');
expect(primary)
.to.have.property('APPEND')
.that.equals('${cbootargs} ${resin_kernel_root} ro rootwait');
});
it('should parse multiple service entries', () => {
const text = stripIndent`\
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
LINUX test1
APPEND test2
LABEL secondary
LINUX test3
APPEND test4\
`;
// @ts-ignore accessing private method
const parsed = ExtlinuxConfigBackend.parseExtlinuxFile(text);
expect(parsed.labels).to.have.property('primary').that.deep.equals({
LINUX: 'test1',
APPEND: 'test2',
});
expect(parsed.labels).to.have.property('secondary').that.deep.equals({
LINUX: 'test3',
APPEND: 'test4',
});
});
it('should parse configuration options from an extlinux.conf file', () => {
let text = stripIndent`\
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=3\
`;
let readFileStub = stub(fs, 'readFile').resolves(text);
let parsed = extlinuxBackend.getBootConfig();
expect(parsed).to.eventually.have.property('isolcpus').that.equals('3');
readFileStub.restore();
text = stripIndent`\
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=3,4,5\
`;
readFileStub = stub(fs, 'readFile').resolves(text);
parsed = extlinuxBackend.getBootConfig();
readFileStub.restore();
expect(parsed)
.to.eventually.have.property('isolcpus')
.that.equals('3,4,5');
});
it('should correctly write to extlinux.conf files', async () => { it('should correctly write to extlinux.conf files', async () => {
stub(fsUtils, 'writeFileAtomic').resolves(); stub(fsUtils, 'writeFileAtomic').resolves();
stub(child_process, 'exec').resolves(); stub(child_process, 'exec').resolves();
@ -316,6 +216,7 @@ describe('Device Backend Config', () => {
const current = {}; const current = {};
const target = { const target = {
HOST_EXTLINUX_isolcpus: '2', HOST_EXTLINUX_isolcpus: '2',
HOST_EXTLINUX_fdt: '/boot/mycustomdtb.dtb',
}; };
expect( expect(
@ -330,14 +231,15 @@ describe('Device Backend Config', () => {
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success'); expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(fsUtils.writeFileAtomic).to.be.calledWith( expect(fsUtils.writeFileAtomic).to.be.calledWith(
'./test/data/mnt/boot/extlinux/extlinux.conf', './test/data/mnt/boot/extlinux/extlinux.conf',
stripIndent`\ stripIndent`
DEFAULT primary\n\ DEFAULT primary
TIMEOUT 30\n\ TIMEOUT 30
MENU TITLE Boot Options\n\ MENU TITLE Boot Options
LABEL primary\n\ LABEL primary
MENU LABEL primary Image\n\ MENU LABEL primary Image
LINUX /Image\n\ LINUX /Image
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2\n\ APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2
FDT /boot/mycustomdtb.dtb
` + '\n', // add newline because stripIndent trims last newline ` + '\n', // add newline because stripIndent trims last newline
); );

View File

@ -6,9 +6,122 @@ import { expect } from './lib/chai-config';
import * as fsUtils from '../src/lib/fs-utils'; import * as fsUtils from '../src/lib/fs-utils';
import { ExtlinuxConfigBackend } from '../src/config/backends/extlinux'; import { ExtlinuxConfigBackend } from '../src/config/backends/extlinux';
describe('EXTLINUX Configuration', () => { describe('Extlinux Configuration', () => {
const backend = new ExtlinuxConfigBackend(); const backend = new ExtlinuxConfigBackend();
it('should parse a extlinux.conf file', () => {
const text = stripIndent`\
DEFAULT primary
# CommentExtlinux files
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
FDT /boot/mycustomdtb.dtb
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait\
`;
// @ts-ignore accessing private method
const parsed = ExtlinuxConfigBackend.parseExtlinuxFile(text);
expect(parsed.globals).to.have.property('DEFAULT').that.equals('primary');
expect(parsed.globals).to.have.property('TIMEOUT').that.equals('30');
expect(parsed.globals)
.to.have.property('MENU TITLE')
.that.equals('Boot Options');
expect(parsed.labels).to.have.property('primary');
const { primary } = parsed.labels;
expect(primary).to.have.property('MENU LABEL').that.equals('primary Image');
expect(primary).to.have.property('LINUX').that.equals('/Image');
expect(primary)
.to.have.property('FDT')
.that.equals('/boot/mycustomdtb.dtb');
expect(primary)
.to.have.property('APPEND')
.that.equals('${cbootargs} ${resin_kernel_root} ro rootwait');
});
it('should parse multiple service entries', () => {
const text = stripIndent`\
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
LINUX test1
FDT /boot/mycustomdtb.dtb
APPEND test2
LABEL secondary
LINUX test3
FDT /boot/mycustomdtb.dtb
APPEND test4\
`;
// @ts-ignore accessing private method
const parsed = ExtlinuxConfigBackend.parseExtlinuxFile(text);
expect(parsed.labels).to.have.property('primary').that.deep.equals({
LINUX: 'test1',
FDT: '/boot/mycustomdtb.dtb',
APPEND: 'test2',
});
expect(parsed.labels).to.have.property('secondary').that.deep.equals({
LINUX: 'test3',
FDT: '/boot/mycustomdtb.dtb',
APPEND: 'test4',
});
});
it('should parse configuration options from an extlinux.conf file', async () => {
let text = stripIndent`\
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
FDT /boot/mycustomdtb.dtb
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=3\
`;
let readFileStub = stub(fs, 'readFile').resolves(text);
let parsed = backend.getBootConfig();
await expect(parsed)
.to.eventually.have.property('isolcpus')
.that.equals('3');
await expect(parsed)
.to.eventually.have.property('fdt')
.that.equals('/boot/mycustomdtb.dtb');
readFileStub.restore();
text = stripIndent`\
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
FDT /boot/mycustomdtb.dtb
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=3,4,5\
`;
readFileStub = stub(fs, 'readFile').resolves(text);
parsed = backend.getBootConfig();
readFileStub.restore();
await expect(parsed)
.to.eventually.have.property('isolcpus')
.that.equals('3,4,5');
});
it('only matches supported devices', () => { it('only matches supported devices', () => {
[ [
{ deviceType: 'jetson-tx', supported: true }, { deviceType: 'jetson-tx', supported: true },
@ -35,9 +148,11 @@ describe('EXTLINUX Configuration', () => {
// Stub bad config // Stub bad config
stub(fs, 'readFile').resolves(badConfig.contents); stub(fs, 'readFile').resolves(badConfig.contents);
// Expect correct rejection from the given bad config // Expect correct rejection from the given bad config
await expect(backend.getBootConfig()).to.eventually.be.rejectedWith( try {
badConfig.reason, await backend.getBootConfig();
); } catch (e) {
expect(e.message).to.equal(badConfig.reason);
}
// Restore stub // Restore stub
(fs.readFile as SinonStub).restore(); (fs.readFile as SinonStub).restore();
} }
@ -49,17 +164,19 @@ describe('EXTLINUX Configuration', () => {
// Stub readFile to return a config that has supported values // Stub readFile to return a config that has supported values
stub(fs, 'readFile').resolves(stripIndent` stub(fs, 'readFile').resolves(stripIndent`
DEFAULT primary\n DEFAULT primary
TIMEOUT 30\n TIMEOUT 30
MENU TITLE Boot Options\n MENU TITLE Boot Options
LABEL primary\n LABEL primary
MENU LABEL primary Image\n MENU LABEL primary Image
LINUX /Image LINUX /Image
FDT /boot/mycustomdtb.dtb
APPEND ro rootwait isolcpus=0,4 APPEND ro rootwait isolcpus=0,4
`); `);
await expect(backend.getBootConfig()).to.eventually.deep.equal({ await expect(backend.getBootConfig()).to.eventually.deep.equal({
isolcpus: '0,4', isolcpus: '0,4',
fdt: '/boot/mycustomdtb.dtb',
}); });
// Restore stub // Restore stub
@ -71,20 +188,21 @@ describe('EXTLINUX Configuration', () => {
stub(child_process, 'exec').resolves(); stub(child_process, 'exec').resolves();
await backend.setBootConfig({ await backend.setBootConfig({
fdt: '/boot/mycustomdtb.dtb',
isolcpus: '2', isolcpus: '2',
randomValueBut: 'that_is_ok', // The backend just sets what it is told. validation is ended in device-config.ts
}); });
expect(fsUtils.writeFileAtomic).to.be.calledWith( expect(fsUtils.writeFileAtomic).to.be.calledWith(
'./test/data/mnt/boot/extlinux/extlinux.conf', './test/data/mnt/boot/extlinux/extlinux.conf',
stripIndent`\ stripIndent`
DEFAULT primary\n\ DEFAULT primary
TIMEOUT 30\n\ TIMEOUT 30
MENU TITLE Boot Options\n\ MENU TITLE Boot Options
LABEL primary\n\ LABEL primary
MENU LABEL primary Image\n\ MENU LABEL primary Image
LINUX /Image\n\ LINUX /Image
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2 randomValueBut=that_is_ok\n\ APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2
FDT /boot/mycustomdtb.dtb
` + '\n', // add newline because stripIndent trims last newline ` + '\n', // add newline because stripIndent trims last newline
); );
@ -96,6 +214,7 @@ describe('EXTLINUX Configuration', () => {
it('only allows supported configuration options', () => { it('only allows supported configuration options', () => {
[ [
{ configName: 'isolcpus', supported: true }, { configName: 'isolcpus', supported: true },
{ configName: 'fdt', supported: true },
{ configName: '', supported: false }, { configName: '', supported: false },
{ configName: 'ro', supported: false }, // not allowed to configure { configName: 'ro', supported: false }, // not allowed to configure
{ configName: 'rootwait', supported: false }, // not allowed to configure { configName: 'rootwait', supported: false }, // not allowed to configure
@ -107,6 +226,7 @@ describe('EXTLINUX Configuration', () => {
it('correctly detects boot config variables', () => { it('correctly detects boot config variables', () => {
[ [
{ config: 'HOST_EXTLINUX_isolcpus', valid: true }, { config: 'HOST_EXTLINUX_isolcpus', valid: true },
{ config: 'HOST_EXTLINUX_fdt', valid: true },
{ config: 'HOST_EXTLINUX_rootwait', valid: true }, { config: 'HOST_EXTLINUX_rootwait', valid: true },
{ config: 'HOST_EXTLINUX_5', valid: true }, { config: 'HOST_EXTLINUX_5', valid: true },
// TO-DO: { config: 'HOST_EXTLINUX', valid: false }, // TO-DO: { config: 'HOST_EXTLINUX', valid: false },
@ -121,6 +241,7 @@ describe('EXTLINUX Configuration', () => {
it('converts variable to backend formatted name', () => { it('converts variable to backend formatted name', () => {
[ [
{ input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' }, { input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' },
{ input: 'HOST_EXTLINUX_fdt', output: 'fdt' },
{ input: 'HOST_EXTLINUX_rootwait', output: 'rootwait' }, { input: 'HOST_EXTLINUX_rootwait', output: 'rootwait' },
{ input: 'HOST_EXTLINUX_something_else', output: 'something_else' }, { input: 'HOST_EXTLINUX_something_else', output: 'something_else' },
{ input: 'HOST_EXTLINUX_', output: 'HOST_EXTLINUX_' }, { input: 'HOST_EXTLINUX_', output: 'HOST_EXTLINUX_' },
@ -144,6 +265,7 @@ describe('EXTLINUX Configuration', () => {
it('returns the environment name for config variable', () => { it('returns the environment name for config variable', () => {
[ [
{ input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' }, { input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' },
{ input: 'fdt', output: 'HOST_EXTLINUX_fdt' },
{ input: 'rootwait', output: 'HOST_EXTLINUX_rootwait' }, { input: 'rootwait', output: 'HOST_EXTLINUX_rootwait' },
{ input: '', output: 'HOST_EXTLINUX_' }, { input: '', output: 'HOST_EXTLINUX_' },
{ input: '5', output: 'HOST_EXTLINUX_5' }, { input: '5', output: 'HOST_EXTLINUX_5' },
@ -156,10 +278,10 @@ describe('EXTLINUX Configuration', () => {
const MALFORMED_CONFIGS = [ const MALFORMED_CONFIGS = [
{ {
contents: stripIndent` contents: stripIndent`
TIMEOUT 30\n TIMEOUT 30
MENU TITLE Boot Options\n MENU TITLE Boot Options
LABEL primary\n LABEL primary
MENU LABEL primary Image\n MENU LABEL primary Image
LINUX /Image LINUX /Image
APPEND ro rootwait isolcpus=0,4 APPEND ro rootwait isolcpus=0,4
`, `,
@ -167,24 +289,23 @@ const MALFORMED_CONFIGS = [
}, },
{ {
contents: stripIndent` contents: stripIndent`
DEFAULT typo_oops\n DEFAULT typo_oops
TIMEOUT 30\n TIMEOUT 30
MENU TITLE Boot Options\n MENU TITLE Boot Options
LABEL primary\n LABEL primary
MENU LABEL primary Image\n MENU LABEL primary Image
LINUX /Image LINUX /Image
APPEND ro rootwait isolcpus=0,4 APPEND ro rootwait isolcpus=0,4
`, `,
reason: reason: 'Cannot find label entry (label: typo_oops) for extlinux.conf file',
'Cannot find default label entry (label: typo_oops) for extlinux.conf file',
}, },
{ {
contents: stripIndent` contents: stripIndent`
DEFAULT primary\n DEFAULT primary
TIMEOUT 30\n TIMEOUT 30
MENU TITLE Boot Options\n MENU TITLE Boot Options
LABEL primary\n LABEL primary
MENU LABEL primary Image\n MENU LABEL primary Image
LINUX /Image LINUX /Image
`, `,
reason: reason:
@ -192,15 +313,14 @@ const MALFORMED_CONFIGS = [
}, },
{ {
contents: stripIndent` contents: stripIndent`
DEFAULT primary\n DEFAULT primary
TIMEOUT 30\n TIMEOUT 30
MENU TITLE Boot Options\n MENU TITLE Boot Options
LABEL primary\n LABEL primary
MENU LABEL primary Image\n MENU LABEL primary Image
LINUX /Image LINUX /Image
APPEND ro rootwait isolcpus=0,4=woops APPEND ro rootwait isolcpus=0,4=woops
`, `,
reason: reason: 'Unable to parse invalid value: isolcpus=0,4=woops',
'Could not parse extlinux configuration entry: ro,rootwait,isolcpus=0,4=woops [value with error: isolcpus=0,4=woops]',
}, },
]; ];

View File

@ -0,0 +1,117 @@
import { AppendDirective } from '../src/config/backends/extlinux-file';
import { expect } from './lib/chai-config';
describe('APPEND directive', () => {
const supportedConfigValues = ['isolcpus'];
const directive = new AppendDirective(supportedConfigValues);
it('parses valid APPEND value', () => {
VALID_VALUES.forEach(({ input, output }) =>
expect(directive.parse(input)).to.deep.equal(output),
);
});
it('errors when parsing invalid APPEND value', () => {
INVALID_VALUES.forEach(({ input, reason }) =>
// @ts-expect-error
expect(() => directive.parse(input)).to.throw(reason),
);
});
it('generates new string from existing string', () => {
expect(
directive.generate(
{
isolcpus: '2',
},
'ro rootwait',
),
).to.deep.equal('ro rootwait isolcpus=2');
});
it('generates string from existing string (replaces values)', () => {
expect(
directive.generate(
{
isolcpus: '2,4',
},
'ro rootwait isolcpus=2',
),
).to.deep.equal('ro rootwait isolcpus=2,4');
});
it('generates string from nothing', () => {
expect(
directive.generate({
isolcpus: '2,4',
}),
).to.deep.equal('isolcpus=2,4');
});
it('generates string from nothing', () => {
expect(
directive.generate({
rootwait: '',
ro: '',
isolcpus: '2,4',
}),
).to.deep.equal('rootwait ro isolcpus=2,4');
});
it('errors when generating with invalid ConfigOptions', () => {
INVALID_CONFIGS_OPTIONS.forEach(({ input, reason }) =>
expect(() => directive.generate(input)).to.throw(reason),
);
});
});
const VALID_VALUES = [
{
input: {
APPEND: '${cbootargs} ${resin_kernel_root} ro rootwait isolcpus=2',
},
output: {
'${cbootargs}': '',
'${resin_kernel_root}': '',
ro: '',
rootwait: '',
isolcpus: '2',
},
},
{
input: {
APPEND: '',
},
output: {},
},
{
input: {
APPEND: 'isolcpus=2,4',
},
output: { isolcpus: '2,4' },
},
];
const INVALID_VALUES = [
{
input: {},
reason:
'Could not find APPEND directive in default extlinux.conf boot entry',
},
{
input: {
APPEND: 'isolcpus=2=4',
},
reason: 'Unable to parse invalid value: isolcpus=2=4',
},
];
const INVALID_CONFIGS_OPTIONS = [
{
input: {
isolcpus: '2,4=',
},
reason:
'One of the values being set contains an invalid character: [ value: 2,4=, key: isolcpus ]',
},
];

View File

@ -0,0 +1,68 @@
import { FDTDirective } from '../src/config/backends/extlinux-file';
import { expect } from './lib/chai-config';
describe('FDT directive', () => {
const directive = new FDTDirective();
it('parses valid FDT value', () => {
VALID_VALUES.forEach(({ input, output }) =>
// @ts-ignore input with no FDT can still be parsed
expect(directive.parse(input)).to.deep.equal(output),
);
});
it('generates value from valid ConfigOptions', () => {
expect(
directive.generate({
fdt: '/boot/mycustomdtb.dtb',
}),
).to.deep.equal('/boot/mycustomdtb.dtb');
});
it('errors when generating with invalid ConfigOptions', () => {
INVALID_CONFIGS_OPTIONS.forEach(({ input, reason }) =>
// @ts-expect-error
expect(() => directive.generate(input)).to.throw(reason),
);
});
});
const VALID_VALUES = [
{
input: {
FDT: '/boot/mycustomdtb.dtb',
},
output: {
fdt: '/boot/mycustomdtb.dtb',
},
},
{
input: {
FDT: '',
},
output: {},
},
{
input: {},
output: {},
},
];
const INVALID_CONFIGS_OPTIONS = [
{
input: {
fdt: '',
},
reason: 'Cannot set FDT of an empty value.',
},
{
input: {
fdt: null,
},
reason: 'Cannot set FDT of non-string value: null',
},
{
input: {},
reason: 'Cannot set FDT of non-string value: ',
},
];