mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-19 03:06:27 +00:00
Move config backend code out to classes which implement a common base
Change-type: patch Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
parent
89627a3392
commit
1e48d02781
@ -1,30 +1,11 @@
|
||||
Promise = require 'bluebird'
|
||||
_ = require 'lodash'
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
|
||||
systemd = require './lib/systemd'
|
||||
{ checkTruthy, checkInt } = require './lib/validation'
|
||||
{ UnitNotLoadedError } = require './lib/errors'
|
||||
configUtils = require './lib/config-utils'
|
||||
|
||||
forbiddenConfigKeys = [
|
||||
'disable_commandline_tags'
|
||||
'cmdline'
|
||||
'kernel'
|
||||
'kernel_address'
|
||||
'kernel_old'
|
||||
'ramfsfile'
|
||||
'ramfsaddr'
|
||||
'initramfs'
|
||||
'device_tree_address'
|
||||
'init_uart_baud'
|
||||
'init_uart_clock'
|
||||
'init_emmc_clock'
|
||||
'boot_delay'
|
||||
'boot_delay_ms'
|
||||
'avoid_safe_mode'
|
||||
]
|
||||
|
||||
vpnServiceName = 'openvpn-resin'
|
||||
|
||||
module.exports = class DeviceConfig
|
||||
@ -64,11 +45,20 @@ module.exports = class DeviceConfig
|
||||
.tapCatch (err) =>
|
||||
@logger.logConfigChange(logValue, { err })
|
||||
setBootConfig: (step) =>
|
||||
@config.get('deviceType')
|
||||
.then (deviceType) =>
|
||||
@setBootConfig(deviceType, step.target)
|
||||
@getConfigBackend()
|
||||
.then (configBackend ) =>
|
||||
@setBootConfig(configBackend, step.target)
|
||||
}
|
||||
@validActions = _.keys(@actionExecutors)
|
||||
@configBackend = null
|
||||
|
||||
getConfigBackend: =>
|
||||
if @configBackend?
|
||||
Promise.resolve(@configBackend)
|
||||
else
|
||||
@config.get('deviceType').then (deviceType) =>
|
||||
@configBackend = configUtils.getConfigBackend(deviceType)
|
||||
return @configBackend
|
||||
|
||||
setTarget: (target, trx) =>
|
||||
db = trx ? @db.models
|
||||
@ -82,10 +72,10 @@ module.exports = class DeviceConfig
|
||||
.then ([ devConfig ]) =>
|
||||
return Promise.all [
|
||||
JSON.parse(devConfig.targetValues)
|
||||
@config.get('deviceType')
|
||||
@getConfigBackend()
|
||||
]
|
||||
.then ([ conf, deviceType ]) =>
|
||||
conf = configUtils.filterConfigKeys(deviceType, @validKeys, conf)
|
||||
.then ([ conf, configBackend ]) =>
|
||||
conf = configUtils.filterConfigKeys(configBackend, @validKeys, conf)
|
||||
if initial or !conf.RESIN_SUPERVISOR_VPN_CONTROL?
|
||||
conf.RESIN_SUPERVISOR_VPN_CONTROL = 'true'
|
||||
for own k, { envVarName, defaultValue } of @configKeys
|
||||
@ -93,11 +83,14 @@ module.exports = class DeviceConfig
|
||||
return conf
|
||||
|
||||
getCurrent: =>
|
||||
@config.getMany([ 'deviceType' ].concat(_.keys(@configKeys)))
|
||||
.then (conf) =>
|
||||
Promise.all [
|
||||
@config.getMany([ 'deviceType' ].concat(_.keys(@configKeys)))
|
||||
@getConfigBackend()
|
||||
]
|
||||
.then ([ conf, configBackend ]) =>
|
||||
Promise.join(
|
||||
@getVPNEnabled()
|
||||
@getBootConfig(conf.deviceType)
|
||||
@getBootConfig(configBackend)
|
||||
(vpnStatus, bootConfig) =>
|
||||
currentConf = {
|
||||
RESIN_SUPERVISOR_VPN_CONTROL: (vpnStatus ? 'true').toString()
|
||||
@ -113,15 +106,17 @@ module.exports = class DeviceConfig
|
||||
RESIN_SUPERVISOR_VPN_CONTROL: 'true'
|
||||
}, _.mapValues(_.mapKeys(@configKeys, 'envVarName'), 'defaultValue'))
|
||||
|
||||
bootConfigChangeRequired: (deviceType, current, target) =>
|
||||
targetBootConfig = configUtils.envToBootConfig(deviceType, target)
|
||||
currentBootConfig = configUtils.envToBootConfig(deviceType, current)
|
||||
bootConfigChangeRequired: (configBackend, current, target) =>
|
||||
targetBootConfig = configUtils.envToBootConfig(configBackend, target)
|
||||
currentBootConfig = configUtils.envToBootConfig(configBackend, current)
|
||||
|
||||
if !_.isEqual(currentBootConfig, targetBootConfig)
|
||||
for key in forbiddenConfigKeys
|
||||
if currentBootConfig[key] != targetBootConfig[key]
|
||||
err = "Attempt to change blacklisted config value #{key}"
|
||||
@logger.logSystemMessage(err, { error: err }, 'Apply boot config error')
|
||||
throw new Error(err)
|
||||
_.each targetBootConfig, (value, key) =>
|
||||
if not configBackend.isSupportedConfig(key)
|
||||
if currentBootConfig[key] != value
|
||||
err = "Attempt to change blacklisted config value #{key}"
|
||||
@logger.logSystemMessage(err, { error: err }, 'Apply boot config error')
|
||||
throw new Error(err)
|
||||
return true
|
||||
return false
|
||||
|
||||
@ -129,8 +124,11 @@ module.exports = class DeviceConfig
|
||||
current = _.clone(currentState.local?.config ? {})
|
||||
target = _.clone(targetState.local?.config ? {})
|
||||
steps = []
|
||||
@config.getMany([ 'deviceType', 'offlineMode' ])
|
||||
.then ({ deviceType, offlineMode }) =>
|
||||
Promise.all [
|
||||
@config.getMany([ 'deviceType', 'offlineMode' ])
|
||||
@getConfigBackend()
|
||||
]
|
||||
.then ([{ deviceType, offlineMode }, configBackend ]) =>
|
||||
configChanges = {}
|
||||
humanReadableConfigChanges = {}
|
||||
match = {
|
||||
@ -160,7 +158,7 @@ module.exports = class DeviceConfig
|
||||
action: 'setVPNEnabled'
|
||||
target: target['RESIN_SUPERVISOR_VPN_CONTROL']
|
||||
})
|
||||
if @bootConfigChangeRequired(deviceType, current, target)
|
||||
if @bootConfigChangeRequired(configBackend, current, target)
|
||||
steps.push({
|
||||
action: 'setBootConfig'
|
||||
target
|
||||
@ -177,27 +175,22 @@ module.exports = class DeviceConfig
|
||||
executeStepAction: (step, opts) =>
|
||||
@actionExecutors[step.action](step, opts)
|
||||
|
||||
readBootConfig: (deviceType) ->
|
||||
fs.readFileAsync(configUtils.getBootConfigPath(deviceType), 'utf8')
|
||||
|
||||
getBootConfig: (deviceType) =>
|
||||
Promise.try =>
|
||||
if !configUtils.isConfigDeviceType(deviceType)
|
||||
getBootConfig: (configBackend) ->
|
||||
Promise.try ->
|
||||
if !configBackend?
|
||||
return {}
|
||||
@readBootConfig(deviceType)
|
||||
.then (configTxt) ->
|
||||
configBackend.getBootConfig()
|
||||
.then (config) ->
|
||||
return configUtils.bootConfigToEnv(configBackend, config)
|
||||
|
||||
config = configUtils.parseBootConfig(deviceType, configTxt)
|
||||
return configUtils.bootConfigToEnv(deviceType, config)
|
||||
|
||||
setBootConfig: (deviceType, target) =>
|
||||
setBootConfig: (configBackend, target) =>
|
||||
Promise.try =>
|
||||
if !configUtils.isConfigDeviceType(deviceType)
|
||||
if !configBackend?
|
||||
return false
|
||||
conf = configUtils.envToBootConfig(deviceType, target)
|
||||
conf = configUtils.envToBootConfig(configBackend, target)
|
||||
@logger.logSystemMessage("Applying boot config: #{JSON.stringify(conf)}", {}, 'Apply boot config in progress')
|
||||
|
||||
configUtils.setBootConfig(deviceType, conf)
|
||||
configBackend.setBootConfig(conf)
|
||||
.then =>
|
||||
@logger.logSystemMessage("Applied boot config: #{JSON.stringify(conf)}", {}, 'Apply boot config success')
|
||||
@rebootRequired = true
|
||||
|
371
src/lib/config-backend.ts
Normal file
371
src/lib/config-backend.ts
Normal file
@ -0,0 +1,371 @@
|
||||
import * as Promise from 'bluebird';
|
||||
import * as childProcessSync from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import { fs } from 'mz';
|
||||
|
||||
import * as constants from './constants';
|
||||
import * as fsUtils from './fs-utils';
|
||||
|
||||
const childProcess: any = Promise.promisifyAll(childProcessSync);
|
||||
|
||||
export interface ConfigOptions {
|
||||
[key: string]: string | string[];
|
||||
}
|
||||
|
||||
interface ExtLinuxFile {
|
||||
labels: {
|
||||
[labelName: string]: {
|
||||
[directive: string]: string;
|
||||
};
|
||||
};
|
||||
globals: { [directive: string]: string };
|
||||
}
|
||||
|
||||
const bootMountPoint = constants.rootMountPoint + constants.bootMountPoint;
|
||||
|
||||
function remountAndWriteAtomic(file: string, data: string): Promise<void> {
|
||||
// TODO: Find out why the below Promise.resolve() is required
|
||||
// Here's the dangerous part:
|
||||
return Promise.resolve(childProcess.execAsync(`mount -t vfat -o remount,rw ${constants.bootBlockDevice} ${bootMountPoint}`))
|
||||
.then(() => {
|
||||
return fsUtils.writeFileAtomic(file, data);
|
||||
})
|
||||
.return();
|
||||
}
|
||||
|
||||
export abstract class DeviceConfigBackend {
|
||||
|
||||
// Does this config backend support the given device type?
|
||||
public abstract matches(deviceType: string): boolean;
|
||||
|
||||
// A function which reads and parses the configuration options from
|
||||
// specific boot config
|
||||
public abstract getBootConfig(): Promise<ConfigOptions>;
|
||||
|
||||
// A function to take a set of options and flush to the configuration
|
||||
// file/backend
|
||||
public abstract setBootConfig(opts: ConfigOptions): Promise<void>;
|
||||
|
||||
// Is the configuration option provided supported by this configuration
|
||||
// backend
|
||||
public abstract isSupportedConfig(configName: string): boolean;
|
||||
|
||||
// Is this variable a boot config variable for this backend?
|
||||
public abstract isBootConfigVar(envVar: string): boolean;
|
||||
|
||||
// Convert a configuration environment variable to a config backend
|
||||
// variable
|
||||
public abstract processConfigVarName(envVar: string): string;
|
||||
|
||||
// Process the value if the environment variable, ready to be written to
|
||||
// the backend
|
||||
public abstract processConfigVarValue(key: string, value: string): string | string[];
|
||||
|
||||
// Return the env var name for this config option
|
||||
public abstract createConfigVarName(configName: string): string;
|
||||
}
|
||||
|
||||
export class RPiConfigBackend extends DeviceConfigBackend {
|
||||
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}CONFIG_`;
|
||||
private static bootConfigPath = `${bootMountPoint}/config.txt`;
|
||||
|
||||
public static bootConfigVarRegex = new RegExp('(' + _.escapeRegExp(RPiConfigBackend.bootConfigVarPrefix) + ')(.+)');
|
||||
|
||||
private static arrayConfigKeys = [
|
||||
'dtparam',
|
||||
'dtoverlay',
|
||||
'device_tree_param',
|
||||
'device_tree_overlay',
|
||||
];
|
||||
private static forbiddenConfigKeys = [
|
||||
'disable_commandline_tags',
|
||||
'cmdline',
|
||||
'kernel',
|
||||
'kernel_address',
|
||||
'kernel_old',
|
||||
'ramfsfile',
|
||||
'ramfsaddr',
|
||||
'initramfs',
|
||||
'device_tree_address',
|
||||
'init_uart_baud',
|
||||
'init_uart_clock',
|
||||
'init_emmc_clock',
|
||||
'boot_delay',
|
||||
'boot_delay_ms',
|
||||
'avoid_safe_mode',
|
||||
];
|
||||
|
||||
public matches(deviceType: string): boolean {
|
||||
return _.startsWith(deviceType, 'raspberry') || deviceType === 'fincm3';
|
||||
}
|
||||
|
||||
public getBootConfig(): Promise<ConfigOptions> {
|
||||
return Promise.resolve(fs.readFile(RPiConfigBackend.bootConfigPath, 'utf-8'))
|
||||
.then((confStr) => {
|
||||
|
||||
const conf: ConfigOptions = { };
|
||||
const configStatements = confStr.split(/\r?\n/);
|
||||
|
||||
for (const configStr of configStatements) {
|
||||
// Don't show warnings for comments and empty lines
|
||||
const trimmed = _.trimStart(configStr);
|
||||
if (_.startsWith(trimmed, '#') || trimmed === '') {
|
||||
continue;
|
||||
}
|
||||
let keyValue = /^([^=]+)=(.*)$/.exec(configStr);
|
||||
if (keyValue != null) {
|
||||
const [ , key, value ] = keyValue;
|
||||
if (!_.includes(RPiConfigBackend.arrayConfigKeys, key)) {
|
||||
conf[key] = value;
|
||||
} else {
|
||||
if (conf[key] == null) {
|
||||
conf[key] = [];
|
||||
}
|
||||
(conf[key] as string[]).push(value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try the next regex instead
|
||||
keyValue = /^(initramfs) (.+)/.exec(configStr);
|
||||
if (keyValue != null) {
|
||||
const [ , key, value ] = keyValue;
|
||||
conf[key] = value;
|
||||
} else {
|
||||
console.log(`Warning - Could not parse config.txt entry: ${configStr}. Ignoring.`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return conf;
|
||||
});
|
||||
}
|
||||
|
||||
public setBootConfig(opts: ConfigOptions): Promise<void> {
|
||||
let confStatements: string[] = [];
|
||||
|
||||
_.each(opts, (value, key) => {
|
||||
|
||||
if (key === 'initramfs') {
|
||||
confStatements.push(`${key} ${value}`);
|
||||
} else if(_.isArray(value)) {
|
||||
confStatements = confStatements.concat(_.map(value, (entry) => `${key}=${entry}`));
|
||||
} else {
|
||||
confStatements.push(`${key}=${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
const confStr = confStatements.join('\n') + '\n';
|
||||
|
||||
return remountAndWriteAtomic(RPiConfigBackend.bootConfigPath, confStr);
|
||||
}
|
||||
|
||||
public isSupportedConfig(configName: string): boolean {
|
||||
return !_.includes(RPiConfigBackend.forbiddenConfigKeys, configName);
|
||||
}
|
||||
|
||||
public isBootConfigVar(envVar: string): boolean {
|
||||
return _.startsWith(envVar, RPiConfigBackend.bootConfigVarPrefix);
|
||||
}
|
||||
|
||||
public processConfigVarName(envVar: string): string {
|
||||
return envVar.replace(RPiConfigBackend.bootConfigVarRegex, '$2');
|
||||
}
|
||||
|
||||
public processConfigVarValue(key: string, value: string): string | string[] {
|
||||
if (_.includes(RPiConfigBackend.arrayConfigKeys, key)) {
|
||||
if (!_.startsWith(value, '"')) {
|
||||
return [ value ];
|
||||
} else {
|
||||
return JSON.parse(`[${value}]`);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public createConfigVarName(configName: string): string {
|
||||
return RPiConfigBackend.bootConfigVarPrefix + configName;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtlinuxConfigBackend extends DeviceConfigBackend {
|
||||
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}EXTLINUX_`;
|
||||
private static bootConfigPath = `${bootMountPoint}/extlinux/extlinux.conf`;
|
||||
|
||||
public static bootConfigVarRegex = new RegExp('(' + _.escapeRegExp(ExtlinuxConfigBackend.bootConfigVarPrefix) + ')(.+)');
|
||||
|
||||
private static suppportedConfigKeys = [
|
||||
'isolcpus',
|
||||
];
|
||||
|
||||
public matches(deviceType: string): boolean {
|
||||
return _.startsWith(deviceType, 'jetson-tx');
|
||||
}
|
||||
|
||||
public getBootConfig(): Promise<ConfigOptions> {
|
||||
return Promise.resolve(fs.readFile(ExtlinuxConfigBackend.bootConfigPath, 'utf-8'))
|
||||
.then((confStr) => {
|
||||
const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile(confStr);
|
||||
|
||||
// First find the default label name
|
||||
const defaultLabel = _.find(parsedBootFile.globals, (_v, l) => {
|
||||
if (l === 'DEFAULT') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (defaultLabel == null) {
|
||||
throw new Error('Could not find default entry for extlinux.conf file');
|
||||
}
|
||||
|
||||
const labelEntry = parsedBootFile.labels[defaultLabel];
|
||||
|
||||
if (labelEntry == null) {
|
||||
throw new Error(`Cannot find default label entry (label: ${defaultLabel}) for extlinux.conf file`);
|
||||
}
|
||||
|
||||
// All configuration options come from the `APPEND` directive in the default label entry
|
||||
const appendEntry = labelEntry.APPEND;
|
||||
|
||||
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 setBootConfig(opts: ConfigOptions): Promise<void> {
|
||||
// First get a representation of the configuration file, with all resin-supported configuration removed
|
||||
return Promise.resolve(fs.readFile(ExtlinuxConfigBackend.bootConfigPath))
|
||||
.then((data) => {
|
||||
const extlinuxFile = ExtlinuxConfigBackend.parseExtlinuxFile(data.toString());
|
||||
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);
|
||||
|
||||
return remountAndWriteAtomic(ExtlinuxConfigBackend.bootConfigPath, extlinuxString);
|
||||
});
|
||||
}
|
||||
|
||||
public isSupportedConfig(configName: string): boolean {
|
||||
return _.includes(ExtlinuxConfigBackend.suppportedConfigKeys, configName);
|
||||
}
|
||||
|
||||
public isBootConfigVar(envVar: string): boolean {
|
||||
return _.startsWith(envVar, ExtlinuxConfigBackend.bootConfigVarPrefix);
|
||||
}
|
||||
|
||||
public processConfigVarName(envVar: string): string {
|
||||
return envVar.replace(ExtlinuxConfigBackend.bootConfigVarRegex, '$2');
|
||||
}
|
||||
|
||||
public processConfigVarValue(_key: string, value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
public createConfigVarName(configName: string): string {
|
||||
return ExtlinuxConfigBackend.bootConfigVarPrefix + configName;
|
||||
}
|
||||
|
||||
private static parseExtlinuxFile(confStr: string): ExtLinuxFile {
|
||||
|
||||
const file: ExtLinuxFile = {
|
||||
globals: { },
|
||||
labels: { },
|
||||
};
|
||||
|
||||
// Firstly split by line and filter any comments and empty lines
|
||||
let lines = confStr.split(/\r?\n/);
|
||||
lines = _.filter(lines, (l) => {
|
||||
const trimmed = _.trimStart(l);
|
||||
return trimmed !== '' && !_.startsWith(trimmed, '#');
|
||||
});
|
||||
|
||||
let lastLabel = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^\s*(\w+)\s?(.*)$/);
|
||||
if (match == null) {
|
||||
console.log('Warning - Could not read extlinux entry: ${line}');
|
||||
continue;
|
||||
}
|
||||
let directive = match[1].toUpperCase();
|
||||
let value = match[2];
|
||||
|
||||
// Special handling for the MENU directive
|
||||
if (directive === 'MENU') {
|
||||
const parts = value.split(' ');
|
||||
directive = 'MENU ' + parts[0];
|
||||
value = parts.slice(1).join(' ');
|
||||
}
|
||||
|
||||
if (directive !== 'LABEL') {
|
||||
if (lastLabel === '') {
|
||||
// Global options
|
||||
file.globals[directive] = value;
|
||||
} else {
|
||||
// Label specific options
|
||||
file.labels[lastLabel][directive] = value;
|
||||
}
|
||||
} else {
|
||||
lastLabel = value;
|
||||
file.labels[lastLabel] = { };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private static extlinuxFileToString(file: ExtLinuxFile): string {
|
||||
let ret = '';
|
||||
_.each(file.globals, (value, directive) => {
|
||||
ret += `${directive} ${value}\n`;
|
||||
});
|
||||
_.each(file.labels, (directives, key) => {
|
||||
ret += `LABEL ${key}\n`;
|
||||
_.each(directives, (value, directive) => {
|
||||
ret += `${directive} ${value}\n`;
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
@ -1,376 +1,68 @@
|
||||
import * as Promise from 'bluebird';
|
||||
import * as childProcessSync from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import { fs } from 'mz';
|
||||
|
||||
import * as constants from './constants';
|
||||
import * as fsUtils from './fs-utils';
|
||||
import {
|
||||
ConfigOptions,
|
||||
DeviceConfigBackend,
|
||||
ExtlinuxConfigBackend,
|
||||
RPiConfigBackend,
|
||||
} from './config-backend';
|
||||
import { EnvVarObject } from './types';
|
||||
|
||||
const childProcess: any = Promise.promisifyAll(childProcessSync);
|
||||
|
||||
export interface ConfigOptions {
|
||||
[key: string]: string | string[];
|
||||
}
|
||||
|
||||
export interface ExtLinuxFile {
|
||||
labels: {
|
||||
[labelName: string]: {
|
||||
[directive: string]: string;
|
||||
};
|
||||
};
|
||||
globals: { [directive: string]: string };
|
||||
}
|
||||
|
||||
export const rpiArrayConfigKeys = [
|
||||
'dtparam',
|
||||
'dtoverlay',
|
||||
'device_tree_param',
|
||||
'device_tree_overlay',
|
||||
const configBackends = [
|
||||
new ExtlinuxConfigBackend(),
|
||||
new RPiConfigBackend(),
|
||||
];
|
||||
|
||||
export const extlinuxSupportedConfig = [
|
||||
'isolcpus',
|
||||
];
|
||||
|
||||
export const hostConfigConfigVarPrefix = 'RESIN_HOST_';
|
||||
export const rPiBootConfigPrefix = hostConfigConfigVarPrefix + 'CONFIG_';
|
||||
export const extlinuxBootConfigPrefix = hostConfigConfigVarPrefix + 'EXTLINUX_';
|
||||
export const rPiConfigRegex = new RegExp('(' + _.escapeRegExp(rPiBootConfigPrefix) + ')(.+)');
|
||||
export const extlinuxConfigRegex = new RegExp('(' + _.escapeRegExp(extlinuxBootConfigPrefix) + ')(.+)');
|
||||
|
||||
export const bootMountPoint = constants.rootMountPoint + constants.bootMountPoint;
|
||||
|
||||
export function isRPiDeviceType(deviceType: string): boolean {
|
||||
return _.startsWith(deviceType, 'raspberry') || deviceType === 'fincm3';
|
||||
}
|
||||
|
||||
export function isExtlinuxDeviceType(deviceType: string): boolean {
|
||||
return _.startsWith(deviceType, 'jetson-tx');
|
||||
}
|
||||
|
||||
export function isConfigDeviceType(deviceType: string): boolean {
|
||||
return isRPiDeviceType(deviceType) || isExtlinuxDeviceType(deviceType);
|
||||
return getConfigBackend(deviceType) != null;
|
||||
}
|
||||
|
||||
export function bootConfigVarPrefix(deviceType: string): string {
|
||||
if (isRPiDeviceType(deviceType)) {
|
||||
return rPiBootConfigPrefix;
|
||||
} else if (isExtlinuxDeviceType(deviceType)) {
|
||||
return extlinuxBootConfigPrefix;
|
||||
} else {
|
||||
throw new Error(`No boot config var prefix for device type: ${deviceType}`);
|
||||
}
|
||||
export function getConfigBackend(deviceType: string): DeviceConfigBackend | undefined {
|
||||
return _.find(configBackends, (backend) => backend.matches(deviceType));
|
||||
}
|
||||
|
||||
export function bootConfigVarRegex(deviceType: string): RegExp {
|
||||
if (isRPiDeviceType(deviceType)) {
|
||||
return rPiConfigRegex;
|
||||
} else if (isExtlinuxDeviceType(deviceType)) {
|
||||
return extlinuxConfigRegex;
|
||||
} else {
|
||||
throw new Error(`No boot config var regex for device type: ${deviceType}`);
|
||||
}
|
||||
}
|
||||
export function envToBootConfig(
|
||||
configBackend: DeviceConfigBackend | null,
|
||||
env: EnvVarObject,
|
||||
): ConfigOptions {
|
||||
|
||||
export function envToBootConfig(deviceType: string, env: EnvVarObject): ConfigOptions {
|
||||
if (!isConfigDeviceType(deviceType)) {
|
||||
if (configBackend == null) {
|
||||
return { };
|
||||
}
|
||||
const prefix = bootConfigVarPrefix(deviceType);
|
||||
const regex = bootConfigVarRegex(deviceType);
|
||||
|
||||
let parsedEnv = _.pickBy(env, (_val: string, key: string) => {
|
||||
return _.startsWith(key, prefix);
|
||||
});
|
||||
parsedEnv = _.mapKeys(parsedEnv, (_val, key) => {
|
||||
return key.replace(regex, '$2');
|
||||
});
|
||||
parsedEnv = _.mapValues(parsedEnv, (val, key) => {
|
||||
if (isRPiDeviceType(deviceType)) {
|
||||
if (_.includes(rpiArrayConfigKeys, key)) {
|
||||
if (!_.startsWith(val, '"')) {
|
||||
return [ val ];
|
||||
} else {
|
||||
return JSON.parse(`[${val}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return val;
|
||||
});
|
||||
|
||||
return parsedEnv as ConfigOptions;
|
||||
return _(env)
|
||||
.pickBy((_val, key) => configBackend.isBootConfigVar(key))
|
||||
.mapKeys((_val, key) => configBackend.processConfigVarName(key))
|
||||
.mapValues((val, key) => configBackend.processConfigVarValue(key, val || ''))
|
||||
.value();
|
||||
}
|
||||
|
||||
export function bootConfigToEnv(deviceType: string, config: ConfigOptions): EnvVarObject {
|
||||
const prefix = bootConfigVarPrefix(deviceType);
|
||||
const confWithEnvKeys = _.mapKeys(config, (_val, key) => {
|
||||
return prefix + key;
|
||||
});
|
||||
return _.mapValues(confWithEnvKeys, (val) => {
|
||||
if (_.isArray(val)) {
|
||||
return JSON.stringify(val).replace(/^\[(.*)\]$/, '$1');
|
||||
}
|
||||
return val;
|
||||
});
|
||||
export function bootConfigToEnv(
|
||||
configBackend: DeviceConfigBackend,
|
||||
config: ConfigOptions,
|
||||
): EnvVarObject {
|
||||
|
||||
return _(config)
|
||||
.mapKeys((_val, key) => configBackend.createConfigVarName(key))
|
||||
.mapValues((val) => {
|
||||
if (_.isArray(val)) {
|
||||
return JSON.stringify(val).replace(/^\[(.*)\]$/, '$1');
|
||||
}
|
||||
return val;
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
export function filterConfigKeys(
|
||||
deviceType: string,
|
||||
configBackend: DeviceConfigBackend | null,
|
||||
allowedKeys: string[],
|
||||
conf: { [key: string]: any },
|
||||
): { [key: string]: any } {
|
||||
|
||||
let isConfigType: boolean = false;
|
||||
let prefix: string;
|
||||
if (isConfigDeviceType(deviceType)) {
|
||||
prefix = bootConfigVarPrefix(deviceType);
|
||||
isConfigType = true;
|
||||
}
|
||||
const isConfigType = configBackend != null;
|
||||
|
||||
return _.pickBy(conf, (_v, k) => {
|
||||
return _.includes(allowedKeys, k) || (isConfigType && _.startsWith(k, prefix));
|
||||
return _.includes(allowedKeys, k) || (isConfigType && configBackend!.isBootConfigVar(k));
|
||||
});
|
||||
}
|
||||
|
||||
export function getBootConfigPath(deviceType: string): string {
|
||||
if (isRPiDeviceType(deviceType)) {
|
||||
return getRpiBootConfig();
|
||||
} else if (isExtlinuxDeviceType(deviceType)) {
|
||||
return getExtlinuxBootConfig();
|
||||
} else {
|
||||
throw new Error(`No boot config exists for device type: ${deviceType}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getRpiBootConfig(): string {
|
||||
return bootMountPoint + '/config.txt';
|
||||
}
|
||||
|
||||
function getExtlinuxBootConfig(): string {
|
||||
return bootMountPoint + '/extlinux/extlinux.conf';
|
||||
}
|
||||
|
||||
export function parseBootConfig(deviceType: string, conf: string): ConfigOptions {
|
||||
if (isRPiDeviceType(deviceType)) {
|
||||
return parseRpiBootConfig(conf);
|
||||
} else if (isExtlinuxDeviceType(deviceType)) {
|
||||
return parseExtlinuxBootConfig(conf);
|
||||
} else {
|
||||
throw new Error(`Cannot parse boot config for device type: ${deviceType}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRpiBootConfig(confStr: string): ConfigOptions {
|
||||
const conf: ConfigOptions = { };
|
||||
const configStatements = confStr.split(/\r?\n/);
|
||||
|
||||
for (const configStr of configStatements) {
|
||||
// Don't show warnings for comments and empty lines
|
||||
const trimmed = _.trimStart(configStr);
|
||||
if (_.startsWith(trimmed, '#') || trimmed === '') {
|
||||
continue;
|
||||
}
|
||||
let keyValue = /^([^#=]+)=(.+)/.exec(configStr);
|
||||
if (keyValue != null) {
|
||||
if (!_.includes(rpiArrayConfigKeys, keyValue[1])) {
|
||||
conf[keyValue[1]] = keyValue[2];
|
||||
} else {
|
||||
const key = keyValue[1];
|
||||
if (conf[key] == null) {
|
||||
conf[key] = [];
|
||||
}
|
||||
(conf[key] as string[]).push(keyValue[2]);
|
||||
}
|
||||
} else {
|
||||
keyValue = /^(initramfs) (.+)/.exec(configStr);
|
||||
if (keyValue != null) {
|
||||
conf[keyValue[1]] = keyValue[2];
|
||||
} else {
|
||||
console.log(`Warning - Could not parse config.txt entry: ${configStr}. Ignoring.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
export function parseExtlinuxBootConfig(confStr: string): ConfigOptions {
|
||||
const parsedBootFile = parseExtlinuxFile(confStr);
|
||||
|
||||
// First find the default label name
|
||||
const defaultLabel = _.find(parsedBootFile.globals, (_v, l) => {
|
||||
if (l === 'DEFAULT') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (defaultLabel == null) {
|
||||
throw new Error('Could not find default entry for extlinux.conf file');
|
||||
}
|
||||
|
||||
const labelEntry = parsedBootFile.labels[defaultLabel];
|
||||
|
||||
if (labelEntry == null) {
|
||||
throw new Error(`Cannot find default label entry (label: ${defaultLabel}) for extlinux.conf file`);
|
||||
}
|
||||
|
||||
// All configuration options come from the `APPEND` directive in the default label entry
|
||||
const appendEntry = labelEntry.APPEND;
|
||||
|
||||
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 (isSupportedExtlinuxConfig(parts[0])) {
|
||||
conf[parts[0]] = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
export function parseExtlinuxFile(confStr: string): ExtLinuxFile {
|
||||
const file: ExtLinuxFile = {
|
||||
globals: { },
|
||||
labels: { },
|
||||
};
|
||||
|
||||
// Firstly split by line and filter any comments and empty lines
|
||||
let lines = confStr.split(/\r?\n/);
|
||||
lines = _.filter(lines, (l) => {
|
||||
const trimmed = _.trimStart(l);
|
||||
return trimmed !== '' && !_.startsWith(trimmed, '#');
|
||||
});
|
||||
|
||||
let lastLabel = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^\s*(\w+)\s?(.*)$/);
|
||||
if (match == null) {
|
||||
console.log('Warning - Could not read extlinux entry: ${line}');
|
||||
continue;
|
||||
}
|
||||
let directive = match[1].toUpperCase();
|
||||
let value = match[2];
|
||||
|
||||
// Special handling for the MENU directive
|
||||
if (directive === 'MENU') {
|
||||
const parts = value.split(' ');
|
||||
directive = 'MENU ' + parts[0];
|
||||
value = parts.slice(1).join(' ');
|
||||
}
|
||||
|
||||
if (directive !== 'LABEL') {
|
||||
if (lastLabel === '') {
|
||||
// Global options
|
||||
file.globals[directive] = value;
|
||||
} else {
|
||||
// Label specific options
|
||||
file.labels[lastLabel][directive] = value;
|
||||
}
|
||||
} else {
|
||||
lastLabel = value;
|
||||
file.labels[lastLabel] = { };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
export function setBootConfig(deviceType: string, target: ConfigOptions): Promise<void> {
|
||||
if (isRPiDeviceType(deviceType)) {
|
||||
return setRpiBootConfig(target);
|
||||
} else if (isExtlinuxDeviceType(deviceType)) {
|
||||
return setExtlinuxBootConfig(target);
|
||||
} else {
|
||||
throw new Error(`Could not set boot config for non-boot config device: ${deviceType}`);
|
||||
}
|
||||
}
|
||||
|
||||
function setRpiBootConfig(target: ConfigOptions): Promise<void> {
|
||||
let confStatements: string[] = [];
|
||||
|
||||
_.each(target, (value, key) => {
|
||||
|
||||
if (key === 'initramfs') {
|
||||
confStatements.push(`${key} ${value}`);
|
||||
} else if(_.isArray(value)) {
|
||||
confStatements = confStatements.concat(_.map(value, (entry) => `${key}=${entry}`));
|
||||
} else {
|
||||
confStatements.push(`${key}=${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
return remountAndWriteAtomic(getRpiBootConfig(), confStatements.join('\n') + '\n');
|
||||
}
|
||||
|
||||
function setExtlinuxBootConfig(target: ConfigOptions): Promise<void> {
|
||||
// First get a representation of the configuration file, with all resin-supported configuration removed
|
||||
return Promise.resolve(fs.readFile(getExtlinuxBootConfig()))
|
||||
.then((data) => {
|
||||
const extlinuxFile = parseExtlinuxFile(data.toString());
|
||||
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 !isSupportedExtlinuxConfig(lhs[0]);
|
||||
});
|
||||
|
||||
// Apply the new configuration to the "plain" append line above
|
||||
|
||||
_.each(target, (value, key) => {
|
||||
appendLine.push(`${key}=${value}`);
|
||||
});
|
||||
|
||||
defaultEntry.APPEND = appendLine.join(' ');
|
||||
const extlinuxString = extlinuxFileToString(extlinuxFile);
|
||||
|
||||
return remountAndWriteAtomic(getExtlinuxBootConfig(), extlinuxString);
|
||||
});
|
||||
}
|
||||
|
||||
function remountAndWriteAtomic(file: string, data: string): Promise<void> {
|
||||
// TODO: Find out why the below Promise.resolve() is required
|
||||
// Here's the dangerous part:
|
||||
return Promise.resolve(childProcess.execAsync(`mount -t vfat -o remount,rw ${constants.bootBlockDevice} ${bootMountPoint}`))
|
||||
.then(() => {
|
||||
return fsUtils.writeFileAtomic(file, data);
|
||||
})
|
||||
.return();
|
||||
}
|
||||
|
||||
function isSupportedExtlinuxConfig(configName: string): boolean {
|
||||
// Currently the supported configuration values come in the form key=value
|
||||
return _.includes(extlinuxSupportedConfig, configName);
|
||||
}
|
||||
|
||||
function extlinuxFileToString(file: ExtLinuxFile): string {
|
||||
let ret = '';
|
||||
_.each(file.globals, (value, directive) => {
|
||||
ret += `${directive} ${value}\n`;
|
||||
});
|
||||
_.each(file.labels, (directives, key) => {
|
||||
ret += `LABEL ${key}\n`;
|
||||
_.each(directives, (value, directive) => {
|
||||
ret += `${directive} ${value}\n`;
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ const constants = {
|
||||
'io.resin.supervised': 'true',
|
||||
},
|
||||
bootBlockDevice: '/dev/mmcblk0p1',
|
||||
hostConfigVarPrefix: 'RESIN_HOST_',
|
||||
};
|
||||
|
||||
if (process.env.DOCKER_HOST == null) {
|
||||
|
@ -9,6 +9,7 @@ prepare = require './lib/prepare'
|
||||
DeviceState = require '../src/device-state'
|
||||
DB = require('../src/db')
|
||||
Config = require('../src/config')
|
||||
{ RPiConfigBackend } = require('../src/lib/config-backend')
|
||||
|
||||
Service = require '../src/compose/service'
|
||||
|
||||
@ -208,6 +209,7 @@ describe 'deviceState', ->
|
||||
err = new Error()
|
||||
err.statusCode = 404
|
||||
throw err
|
||||
@deviceState.deviceConfig.configBackend = new RPiConfigBackend()
|
||||
@db.init()
|
||||
.then =>
|
||||
@config.init()
|
||||
|
@ -1,13 +1,18 @@
|
||||
Promise = require 'bluebird'
|
||||
prepare = require './lib/prepare'
|
||||
{ fs } = require 'mz'
|
||||
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
{ stub, spy } = m.sinon
|
||||
|
||||
prepare = require './lib/prepare'
|
||||
fsUtils = require '../src/lib/fs-utils'
|
||||
|
||||
DeviceConfig = require '../src/device-config'
|
||||
{ ExtlinuxConfigBackend, RPiConfigBackend } = require '../src/lib/config-backend'
|
||||
|
||||
extlinuxBackend = new ExtlinuxConfigBackend()
|
||||
rpiConfigBackend = new RPiConfigBackend()
|
||||
|
||||
childProcess = require 'child_process'
|
||||
|
||||
@ -25,7 +30,8 @@ describe 'DeviceConfig', ->
|
||||
|
||||
# Test that the format for special values like initramfs and array variables is parsed correctly
|
||||
it 'allows getting boot config with getBootConfig', ->
|
||||
stub(@deviceConfig, 'readBootConfig').resolves('\
|
||||
|
||||
stub(fs, 'readFile').resolves('\
|
||||
initramfs initramf.gz 0x00800000\n\
|
||||
dtparam=i2c=on\n\
|
||||
dtparam=audio=on\n\
|
||||
@ -33,9 +39,9 @@ describe 'DeviceConfig', ->
|
||||
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13\n\
|
||||
foobar=baz\n\
|
||||
')
|
||||
@deviceConfig.getBootConfig('raspberry-pi')
|
||||
.then (conf) =>
|
||||
@deviceConfig.readBootConfig.restore()
|
||||
@deviceConfig.getBootConfig(rpiConfigBackend)
|
||||
.then (conf) ->
|
||||
fs.readFile.restore()
|
||||
expect(conf).to.deep.equal({
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||
@ -44,7 +50,7 @@ describe 'DeviceConfig', ->
|
||||
})
|
||||
|
||||
it 'properly reads a real config.txt file', ->
|
||||
@deviceConfig.getBootConfig('raspberrypi3')
|
||||
@deviceConfig.getBootConfig(rpiConfigBackend)
|
||||
.then (conf) ->
|
||||
expect(conf).to.deep.equal({
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c_arm=on","spi=on","audio=on"'
|
||||
@ -69,7 +75,7 @@ describe 'DeviceConfig', ->
|
||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||
}
|
||||
promise = Promise.try =>
|
||||
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
||||
@deviceConfig.bootConfigChangeRequired(rpiConfigBackend, current, target)
|
||||
expect(promise).to.be.rejected
|
||||
promise.catch (err) =>
|
||||
expect(@fakeLogger.logSystemMessage).to.be.calledOnce
|
||||
@ -92,7 +98,7 @@ describe 'DeviceConfig', ->
|
||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||
}
|
||||
promise = Promise.try =>
|
||||
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
||||
@deviceConfig.bootConfigChangeRequired(rpiConfigBackend, current, target)
|
||||
expect(promise).to.eventually.equal(false)
|
||||
promise.then =>
|
||||
expect(@fakeLogger.logSystemMessage).to.not.be.called
|
||||
@ -115,10 +121,10 @@ describe 'DeviceConfig', ->
|
||||
RESIN_HOST_CONFIG_foobaz: 'bar'
|
||||
}
|
||||
promise = Promise.try =>
|
||||
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
||||
@deviceConfig.bootConfigChangeRequired(rpiConfigBackend, current, target)
|
||||
expect(promise).to.eventually.equal(true)
|
||||
promise.then =>
|
||||
@deviceConfig.setBootConfig('raspberry-pi', target)
|
||||
@deviceConfig.setBootConfig(rpiConfigBackend, target)
|
||||
.then =>
|
||||
expect(childProcess.execAsync).to.be.calledOnce
|
||||
expect(@fakeLogger.logSystemMessage).to.be.calledTwice
|
||||
@ -148,10 +154,10 @@ describe 'DeviceConfig', ->
|
||||
}
|
||||
|
||||
promise = Promise.try =>
|
||||
@deviceConfig.bootConfigChangeRequired('jetson-tx2', current, target)
|
||||
@deviceConfig.bootConfigChangeRequired(extlinuxBackend, current, target)
|
||||
expect(promise).to.eventually.equal(true)
|
||||
promise.then =>
|
||||
@deviceConfig.setBootConfig('jetson-tx2', target)
|
||||
@deviceConfig.setBootConfig(extlinuxBackend, target)
|
||||
.then =>
|
||||
expect(childProcess.execAsync).to.be.calledOnce
|
||||
expect(@fakeLogger.logSystemMessage).to.be.calledTwice
|
||||
|
@ -1,7 +1,14 @@
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
{ stub } = m.sinon
|
||||
|
||||
{ fs } = require 'mz'
|
||||
|
||||
configUtils = require '../src/lib/config-utils'
|
||||
{ ExtlinuxConfigBackend, RPiConfigBackend } = require '../src/lib/config-backend'
|
||||
|
||||
extlinuxBackend = new ExtlinuxConfigBackend()
|
||||
rpiBackend = new RPiConfigBackend()
|
||||
|
||||
describe 'Config Utilities', ->
|
||||
|
||||
@ -10,7 +17,7 @@ describe 'Config Utilities', ->
|
||||
describe 'Env <-> Config', ->
|
||||
|
||||
it 'correctly transforms environments to boot config objects', ->
|
||||
bootConfig = configUtils.envToBootConfig('raspberry-pi', {
|
||||
bootConfig = configUtils.envToBootConfig(rpiBackend, {
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||
@ -38,7 +45,7 @@ describe 'Config Utilities', ->
|
||||
APPEND ${cbootargs} ${resin_kernel_root} ro rootwait
|
||||
'''
|
||||
|
||||
parsed = configUtils.parseExtlinuxFile(text)
|
||||
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')
|
||||
@ -64,7 +71,7 @@ describe 'Config Utilities', ->
|
||||
APPEND test4
|
||||
'''
|
||||
|
||||
parsed = configUtils.parseExtlinuxFile(text)
|
||||
parsed = ExtlinuxConfigBackend.parseExtlinuxFile(text)
|
||||
expect(parsed.labels).to.have.property('primary').that.deep.equals({
|
||||
LINUX: 'test1'
|
||||
APPEND: 'test2'
|
||||
@ -87,9 +94,11 @@ describe 'Config Utilities', ->
|
||||
APPEND ${cbootargs} ${resin_kernel_root} ro rootwait isolcpus=3
|
||||
'''
|
||||
|
||||
parsed = configUtils.parseExtlinuxBootConfig(text)
|
||||
stub(fs, 'readFile').resolves(text)
|
||||
parsed = extlinuxBackend.getBootConfig()
|
||||
|
||||
expect(parsed).to.have.property('isolcpus').that.equals('3')
|
||||
expect(parsed).to.eventually.have.property('isolcpus').that.equals('3')
|
||||
fs.readFile.restore()
|
||||
|
||||
|
||||
text = '''
|
||||
@ -103,7 +112,10 @@ describe 'Config Utilities', ->
|
||||
LINUX /Image
|
||||
APPEND ${cbootargs} ${resin_kernel_root} ro rootwait isolcpus=3,4,5
|
||||
'''
|
||||
stub(fs, 'readFile').resolves(text)
|
||||
|
||||
parsed = configUtils.parseExtlinuxBootConfig(text)
|
||||
parsed = extlinuxBackend.getBootConfig()
|
||||
|
||||
expect(parsed).to.have.property('isolcpus').that.equals('3,4,5')
|
||||
fs.readFile.restore()
|
||||
|
||||
expect(parsed).to.eventually.have.property('isolcpus').that.equals('3,4,5')
|
||||
|
Loading…
Reference in New Issue
Block a user