mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-23 23:42:29 +00:00
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:
parent
3098abeca5
commit
59fc589eb2
@ -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,
|
@ -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;
|
||||||
}
|
}
|
||||||
|
166
src/config/backends/extlinux-file.ts
Normal file
166
src/config/backends/extlinux-file.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}]`);
|
||||||
|
@ -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(),
|
||||||
|
@ -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';
|
||||||
|
@ -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 {}
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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]',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
117
test/29-append-directive.spec.ts
Normal file
117
test/29-append-directive.spec.ts
Normal 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 ]',
|
||||||
|
},
|
||||||
|
];
|
68
test/30-fdt-directive.spec.ts
Normal file
68
test/30-fdt-directive.spec.ts
Normal 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: ',
|
||||||
|
},
|
||||||
|
];
|
Loading…
Reference in New Issue
Block a user