mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-11 20:11:42 +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:
@ -1,30 +1,11 @@
|
|||||||
Promise = require 'bluebird'
|
Promise = require 'bluebird'
|
||||||
_ = require 'lodash'
|
_ = require 'lodash'
|
||||||
fs = Promise.promisifyAll(require('fs'))
|
|
||||||
|
|
||||||
systemd = require './lib/systemd'
|
systemd = require './lib/systemd'
|
||||||
{ checkTruthy, checkInt } = require './lib/validation'
|
{ checkTruthy, checkInt } = require './lib/validation'
|
||||||
{ UnitNotLoadedError } = require './lib/errors'
|
{ UnitNotLoadedError } = require './lib/errors'
|
||||||
configUtils = require './lib/config-utils'
|
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'
|
vpnServiceName = 'openvpn-resin'
|
||||||
|
|
||||||
module.exports = class DeviceConfig
|
module.exports = class DeviceConfig
|
||||||
@ -64,11 +45,20 @@ module.exports = class DeviceConfig
|
|||||||
.tapCatch (err) =>
|
.tapCatch (err) =>
|
||||||
@logger.logConfigChange(logValue, { err })
|
@logger.logConfigChange(logValue, { err })
|
||||||
setBootConfig: (step) =>
|
setBootConfig: (step) =>
|
||||||
@config.get('deviceType')
|
@getConfigBackend()
|
||||||
.then (deviceType) =>
|
.then (configBackend ) =>
|
||||||
@setBootConfig(deviceType, step.target)
|
@setBootConfig(configBackend, step.target)
|
||||||
}
|
}
|
||||||
@validActions = _.keys(@actionExecutors)
|
@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) =>
|
setTarget: (target, trx) =>
|
||||||
db = trx ? @db.models
|
db = trx ? @db.models
|
||||||
@ -82,10 +72,10 @@ module.exports = class DeviceConfig
|
|||||||
.then ([ devConfig ]) =>
|
.then ([ devConfig ]) =>
|
||||||
return Promise.all [
|
return Promise.all [
|
||||||
JSON.parse(devConfig.targetValues)
|
JSON.parse(devConfig.targetValues)
|
||||||
@config.get('deviceType')
|
@getConfigBackend()
|
||||||
]
|
]
|
||||||
.then ([ conf, deviceType ]) =>
|
.then ([ conf, configBackend ]) =>
|
||||||
conf = configUtils.filterConfigKeys(deviceType, @validKeys, conf)
|
conf = configUtils.filterConfigKeys(configBackend, @validKeys, conf)
|
||||||
if initial or !conf.RESIN_SUPERVISOR_VPN_CONTROL?
|
if initial or !conf.RESIN_SUPERVISOR_VPN_CONTROL?
|
||||||
conf.RESIN_SUPERVISOR_VPN_CONTROL = 'true'
|
conf.RESIN_SUPERVISOR_VPN_CONTROL = 'true'
|
||||||
for own k, { envVarName, defaultValue } of @configKeys
|
for own k, { envVarName, defaultValue } of @configKeys
|
||||||
@ -93,11 +83,14 @@ module.exports = class DeviceConfig
|
|||||||
return conf
|
return conf
|
||||||
|
|
||||||
getCurrent: =>
|
getCurrent: =>
|
||||||
@config.getMany([ 'deviceType' ].concat(_.keys(@configKeys)))
|
Promise.all [
|
||||||
.then (conf) =>
|
@config.getMany([ 'deviceType' ].concat(_.keys(@configKeys)))
|
||||||
|
@getConfigBackend()
|
||||||
|
]
|
||||||
|
.then ([ conf, configBackend ]) =>
|
||||||
Promise.join(
|
Promise.join(
|
||||||
@getVPNEnabled()
|
@getVPNEnabled()
|
||||||
@getBootConfig(conf.deviceType)
|
@getBootConfig(configBackend)
|
||||||
(vpnStatus, bootConfig) =>
|
(vpnStatus, bootConfig) =>
|
||||||
currentConf = {
|
currentConf = {
|
||||||
RESIN_SUPERVISOR_VPN_CONTROL: (vpnStatus ? 'true').toString()
|
RESIN_SUPERVISOR_VPN_CONTROL: (vpnStatus ? 'true').toString()
|
||||||
@ -113,15 +106,17 @@ module.exports = class DeviceConfig
|
|||||||
RESIN_SUPERVISOR_VPN_CONTROL: 'true'
|
RESIN_SUPERVISOR_VPN_CONTROL: 'true'
|
||||||
}, _.mapValues(_.mapKeys(@configKeys, 'envVarName'), 'defaultValue'))
|
}, _.mapValues(_.mapKeys(@configKeys, 'envVarName'), 'defaultValue'))
|
||||||
|
|
||||||
bootConfigChangeRequired: (deviceType, current, target) =>
|
bootConfigChangeRequired: (configBackend, current, target) =>
|
||||||
targetBootConfig = configUtils.envToBootConfig(deviceType, target)
|
targetBootConfig = configUtils.envToBootConfig(configBackend, target)
|
||||||
currentBootConfig = configUtils.envToBootConfig(deviceType, current)
|
currentBootConfig = configUtils.envToBootConfig(configBackend, current)
|
||||||
|
|
||||||
if !_.isEqual(currentBootConfig, targetBootConfig)
|
if !_.isEqual(currentBootConfig, targetBootConfig)
|
||||||
for key in forbiddenConfigKeys
|
_.each targetBootConfig, (value, key) =>
|
||||||
if currentBootConfig[key] != targetBootConfig[key]
|
if not configBackend.isSupportedConfig(key)
|
||||||
err = "Attempt to change blacklisted config value #{key}"
|
if currentBootConfig[key] != value
|
||||||
@logger.logSystemMessage(err, { error: err }, 'Apply boot config error')
|
err = "Attempt to change blacklisted config value #{key}"
|
||||||
throw new Error(err)
|
@logger.logSystemMessage(err, { error: err }, 'Apply boot config error')
|
||||||
|
throw new Error(err)
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
@ -129,8 +124,11 @@ module.exports = class DeviceConfig
|
|||||||
current = _.clone(currentState.local?.config ? {})
|
current = _.clone(currentState.local?.config ? {})
|
||||||
target = _.clone(targetState.local?.config ? {})
|
target = _.clone(targetState.local?.config ? {})
|
||||||
steps = []
|
steps = []
|
||||||
@config.getMany([ 'deviceType', 'offlineMode' ])
|
Promise.all [
|
||||||
.then ({ deviceType, offlineMode }) =>
|
@config.getMany([ 'deviceType', 'offlineMode' ])
|
||||||
|
@getConfigBackend()
|
||||||
|
]
|
||||||
|
.then ([{ deviceType, offlineMode }, configBackend ]) =>
|
||||||
configChanges = {}
|
configChanges = {}
|
||||||
humanReadableConfigChanges = {}
|
humanReadableConfigChanges = {}
|
||||||
match = {
|
match = {
|
||||||
@ -160,7 +158,7 @@ module.exports = class DeviceConfig
|
|||||||
action: 'setVPNEnabled'
|
action: 'setVPNEnabled'
|
||||||
target: target['RESIN_SUPERVISOR_VPN_CONTROL']
|
target: target['RESIN_SUPERVISOR_VPN_CONTROL']
|
||||||
})
|
})
|
||||||
if @bootConfigChangeRequired(deviceType, current, target)
|
if @bootConfigChangeRequired(configBackend, current, target)
|
||||||
steps.push({
|
steps.push({
|
||||||
action: 'setBootConfig'
|
action: 'setBootConfig'
|
||||||
target
|
target
|
||||||
@ -177,27 +175,22 @@ module.exports = class DeviceConfig
|
|||||||
executeStepAction: (step, opts) =>
|
executeStepAction: (step, opts) =>
|
||||||
@actionExecutors[step.action](step, opts)
|
@actionExecutors[step.action](step, opts)
|
||||||
|
|
||||||
readBootConfig: (deviceType) ->
|
getBootConfig: (configBackend) ->
|
||||||
fs.readFileAsync(configUtils.getBootConfigPath(deviceType), 'utf8')
|
Promise.try ->
|
||||||
|
if !configBackend?
|
||||||
getBootConfig: (deviceType) =>
|
|
||||||
Promise.try =>
|
|
||||||
if !configUtils.isConfigDeviceType(deviceType)
|
|
||||||
return {}
|
return {}
|
||||||
@readBootConfig(deviceType)
|
configBackend.getBootConfig()
|
||||||
.then (configTxt) ->
|
.then (config) ->
|
||||||
|
return configUtils.bootConfigToEnv(configBackend, config)
|
||||||
|
|
||||||
config = configUtils.parseBootConfig(deviceType, configTxt)
|
setBootConfig: (configBackend, target) =>
|
||||||
return configUtils.bootConfigToEnv(deviceType, config)
|
|
||||||
|
|
||||||
setBootConfig: (deviceType, target) =>
|
|
||||||
Promise.try =>
|
Promise.try =>
|
||||||
if !configUtils.isConfigDeviceType(deviceType)
|
if !configBackend?
|
||||||
return false
|
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')
|
@logger.logSystemMessage("Applying boot config: #{JSON.stringify(conf)}", {}, 'Apply boot config in progress')
|
||||||
|
|
||||||
configUtils.setBootConfig(deviceType, conf)
|
configBackend.setBootConfig(conf)
|
||||||
.then =>
|
.then =>
|
||||||
@logger.logSystemMessage("Applied boot config: #{JSON.stringify(conf)}", {}, 'Apply boot config success')
|
@logger.logSystemMessage("Applied boot config: #{JSON.stringify(conf)}", {}, 'Apply boot config success')
|
||||||
@rebootRequired = true
|
@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 * as _ from 'lodash';
|
||||||
import { fs } from 'mz';
|
|
||||||
|
|
||||||
import * as constants from './constants';
|
import {
|
||||||
import * as fsUtils from './fs-utils';
|
ConfigOptions,
|
||||||
|
DeviceConfigBackend,
|
||||||
|
ExtlinuxConfigBackend,
|
||||||
|
RPiConfigBackend,
|
||||||
|
} from './config-backend';
|
||||||
import { EnvVarObject } from './types';
|
import { EnvVarObject } from './types';
|
||||||
|
|
||||||
const childProcess: any = Promise.promisifyAll(childProcessSync);
|
|
||||||
|
|
||||||
export interface ConfigOptions {
|
const configBackends = [
|
||||||
[key: string]: string | string[];
|
new ExtlinuxConfigBackend(),
|
||||||
}
|
new RPiConfigBackend(),
|
||||||
|
|
||||||
export interface ExtLinuxFile {
|
|
||||||
labels: {
|
|
||||||
[labelName: string]: {
|
|
||||||
[directive: string]: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
globals: { [directive: string]: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rpiArrayConfigKeys = [
|
|
||||||
'dtparam',
|
|
||||||
'dtoverlay',
|
|
||||||
'device_tree_param',
|
|
||||||
'device_tree_overlay',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
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 {
|
export function isConfigDeviceType(deviceType: string): boolean {
|
||||||
return isRPiDeviceType(deviceType) || isExtlinuxDeviceType(deviceType);
|
return getConfigBackend(deviceType) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bootConfigVarPrefix(deviceType: string): string {
|
export function getConfigBackend(deviceType: string): DeviceConfigBackend | undefined {
|
||||||
if (isRPiDeviceType(deviceType)) {
|
return _.find(configBackends, (backend) => backend.matches(deviceType));
|
||||||
return rPiBootConfigPrefix;
|
|
||||||
} else if (isExtlinuxDeviceType(deviceType)) {
|
|
||||||
return extlinuxBootConfigPrefix;
|
|
||||||
} else {
|
|
||||||
throw new Error(`No boot config var prefix for device type: ${deviceType}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bootConfigVarRegex(deviceType: string): RegExp {
|
export function envToBootConfig(
|
||||||
if (isRPiDeviceType(deviceType)) {
|
configBackend: DeviceConfigBackend | null,
|
||||||
return rPiConfigRegex;
|
env: EnvVarObject,
|
||||||
} else if (isExtlinuxDeviceType(deviceType)) {
|
): ConfigOptions {
|
||||||
return extlinuxConfigRegex;
|
|
||||||
} else {
|
|
||||||
throw new Error(`No boot config var regex for device type: ${deviceType}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function envToBootConfig(deviceType: string, env: EnvVarObject): ConfigOptions {
|
if (configBackend == null) {
|
||||||
if (!isConfigDeviceType(deviceType)) {
|
|
||||||
return { };
|
return { };
|
||||||
}
|
}
|
||||||
const prefix = bootConfigVarPrefix(deviceType);
|
|
||||||
const regex = bootConfigVarRegex(deviceType);
|
|
||||||
|
|
||||||
let parsedEnv = _.pickBy(env, (_val: string, key: string) => {
|
return _(env)
|
||||||
return _.startsWith(key, prefix);
|
.pickBy((_val, key) => configBackend.isBootConfigVar(key))
|
||||||
});
|
.mapKeys((_val, key) => configBackend.processConfigVarName(key))
|
||||||
parsedEnv = _.mapKeys(parsedEnv, (_val, key) => {
|
.mapValues((val, key) => configBackend.processConfigVarValue(key, val || ''))
|
||||||
return key.replace(regex, '$2');
|
.value();
|
||||||
});
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bootConfigToEnv(deviceType: string, config: ConfigOptions): EnvVarObject {
|
export function bootConfigToEnv(
|
||||||
const prefix = bootConfigVarPrefix(deviceType);
|
configBackend: DeviceConfigBackend,
|
||||||
const confWithEnvKeys = _.mapKeys(config, (_val, key) => {
|
config: ConfigOptions,
|
||||||
return prefix + key;
|
): EnvVarObject {
|
||||||
});
|
|
||||||
return _.mapValues(confWithEnvKeys, (val) => {
|
return _(config)
|
||||||
if (_.isArray(val)) {
|
.mapKeys((_val, key) => configBackend.createConfigVarName(key))
|
||||||
return JSON.stringify(val).replace(/^\[(.*)\]$/, '$1');
|
.mapValues((val) => {
|
||||||
}
|
if (_.isArray(val)) {
|
||||||
return val;
|
return JSON.stringify(val).replace(/^\[(.*)\]$/, '$1');
|
||||||
});
|
}
|
||||||
|
return val;
|
||||||
|
})
|
||||||
|
.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterConfigKeys(
|
export function filterConfigKeys(
|
||||||
deviceType: string,
|
configBackend: DeviceConfigBackend | null,
|
||||||
allowedKeys: string[],
|
allowedKeys: string[],
|
||||||
conf: { [key: string]: any },
|
conf: { [key: string]: any },
|
||||||
): { [key: string]: any } {
|
): { [key: string]: any } {
|
||||||
|
|
||||||
let isConfigType: boolean = false;
|
const isConfigType = configBackend != null;
|
||||||
let prefix: string;
|
|
||||||
if (isConfigDeviceType(deviceType)) {
|
|
||||||
prefix = bootConfigVarPrefix(deviceType);
|
|
||||||
isConfigType = true;
|
|
||||||
}
|
|
||||||
return _.pickBy(conf, (_v, k) => {
|
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',
|
'io.resin.supervised': 'true',
|
||||||
},
|
},
|
||||||
bootBlockDevice: '/dev/mmcblk0p1',
|
bootBlockDevice: '/dev/mmcblk0p1',
|
||||||
|
hostConfigVarPrefix: 'RESIN_HOST_',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.DOCKER_HOST == null) {
|
if (process.env.DOCKER_HOST == null) {
|
||||||
|
@ -9,6 +9,7 @@ prepare = require './lib/prepare'
|
|||||||
DeviceState = require '../src/device-state'
|
DeviceState = require '../src/device-state'
|
||||||
DB = require('../src/db')
|
DB = require('../src/db')
|
||||||
Config = require('../src/config')
|
Config = require('../src/config')
|
||||||
|
{ RPiConfigBackend } = require('../src/lib/config-backend')
|
||||||
|
|
||||||
Service = require '../src/compose/service'
|
Service = require '../src/compose/service'
|
||||||
|
|
||||||
@ -208,6 +209,7 @@ describe 'deviceState', ->
|
|||||||
err = new Error()
|
err = new Error()
|
||||||
err.statusCode = 404
|
err.statusCode = 404
|
||||||
throw err
|
throw err
|
||||||
|
@deviceState.deviceConfig.configBackend = new RPiConfigBackend()
|
||||||
@db.init()
|
@db.init()
|
||||||
.then =>
|
.then =>
|
||||||
@config.init()
|
@config.init()
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
Promise = require 'bluebird'
|
Promise = require 'bluebird'
|
||||||
prepare = require './lib/prepare'
|
{ fs } = require 'mz'
|
||||||
|
|
||||||
m = require 'mochainon'
|
m = require 'mochainon'
|
||||||
{ expect } = m.chai
|
{ expect } = m.chai
|
||||||
{ stub, spy } = m.sinon
|
{ stub, spy } = m.sinon
|
||||||
|
|
||||||
|
prepare = require './lib/prepare'
|
||||||
fsUtils = require '../src/lib/fs-utils'
|
fsUtils = require '../src/lib/fs-utils'
|
||||||
|
|
||||||
DeviceConfig = require '../src/device-config'
|
DeviceConfig = require '../src/device-config'
|
||||||
|
{ ExtlinuxConfigBackend, RPiConfigBackend } = require '../src/lib/config-backend'
|
||||||
|
|
||||||
|
extlinuxBackend = new ExtlinuxConfigBackend()
|
||||||
|
rpiConfigBackend = new RPiConfigBackend()
|
||||||
|
|
||||||
childProcess = require 'child_process'
|
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
|
# Test that the format for special values like initramfs and array variables is parsed correctly
|
||||||
it 'allows getting boot config with getBootConfig', ->
|
it 'allows getting boot config with getBootConfig', ->
|
||||||
stub(@deviceConfig, 'readBootConfig').resolves('\
|
|
||||||
|
stub(fs, 'readFile').resolves('\
|
||||||
initramfs initramf.gz 0x00800000\n\
|
initramfs initramf.gz 0x00800000\n\
|
||||||
dtparam=i2c=on\n\
|
dtparam=i2c=on\n\
|
||||||
dtparam=audio=on\n\
|
dtparam=audio=on\n\
|
||||||
@ -33,9 +39,9 @@ describe 'DeviceConfig', ->
|
|||||||
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13\n\
|
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13\n\
|
||||||
foobar=baz\n\
|
foobar=baz\n\
|
||||||
')
|
')
|
||||||
@deviceConfig.getBootConfig('raspberry-pi')
|
@deviceConfig.getBootConfig(rpiConfigBackend)
|
||||||
.then (conf) =>
|
.then (conf) ->
|
||||||
@deviceConfig.readBootConfig.restore()
|
fs.readFile.restore()
|
||||||
expect(conf).to.deep.equal({
|
expect(conf).to.deep.equal({
|
||||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||||
@ -44,7 +50,7 @@ describe 'DeviceConfig', ->
|
|||||||
})
|
})
|
||||||
|
|
||||||
it 'properly reads a real config.txt file', ->
|
it 'properly reads a real config.txt file', ->
|
||||||
@deviceConfig.getBootConfig('raspberrypi3')
|
@deviceConfig.getBootConfig(rpiConfigBackend)
|
||||||
.then (conf) ->
|
.then (conf) ->
|
||||||
expect(conf).to.deep.equal({
|
expect(conf).to.deep.equal({
|
||||||
RESIN_HOST_CONFIG_dtparam: '"i2c_arm=on","spi=on","audio=on"'
|
RESIN_HOST_CONFIG_dtparam: '"i2c_arm=on","spi=on","audio=on"'
|
||||||
@ -69,7 +75,7 @@ describe 'DeviceConfig', ->
|
|||||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||||
}
|
}
|
||||||
promise = Promise.try =>
|
promise = Promise.try =>
|
||||||
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
@deviceConfig.bootConfigChangeRequired(rpiConfigBackend, current, target)
|
||||||
expect(promise).to.be.rejected
|
expect(promise).to.be.rejected
|
||||||
promise.catch (err) =>
|
promise.catch (err) =>
|
||||||
expect(@fakeLogger.logSystemMessage).to.be.calledOnce
|
expect(@fakeLogger.logSystemMessage).to.be.calledOnce
|
||||||
@ -92,7 +98,7 @@ describe 'DeviceConfig', ->
|
|||||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||||
}
|
}
|
||||||
promise = Promise.try =>
|
promise = Promise.try =>
|
||||||
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
@deviceConfig.bootConfigChangeRequired(rpiConfigBackend, current, target)
|
||||||
expect(promise).to.eventually.equal(false)
|
expect(promise).to.eventually.equal(false)
|
||||||
promise.then =>
|
promise.then =>
|
||||||
expect(@fakeLogger.logSystemMessage).to.not.be.called
|
expect(@fakeLogger.logSystemMessage).to.not.be.called
|
||||||
@ -115,10 +121,10 @@ describe 'DeviceConfig', ->
|
|||||||
RESIN_HOST_CONFIG_foobaz: 'bar'
|
RESIN_HOST_CONFIG_foobaz: 'bar'
|
||||||
}
|
}
|
||||||
promise = Promise.try =>
|
promise = Promise.try =>
|
||||||
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
@deviceConfig.bootConfigChangeRequired(rpiConfigBackend, current, target)
|
||||||
expect(promise).to.eventually.equal(true)
|
expect(promise).to.eventually.equal(true)
|
||||||
promise.then =>
|
promise.then =>
|
||||||
@deviceConfig.setBootConfig('raspberry-pi', target)
|
@deviceConfig.setBootConfig(rpiConfigBackend, target)
|
||||||
.then =>
|
.then =>
|
||||||
expect(childProcess.execAsync).to.be.calledOnce
|
expect(childProcess.execAsync).to.be.calledOnce
|
||||||
expect(@fakeLogger.logSystemMessage).to.be.calledTwice
|
expect(@fakeLogger.logSystemMessage).to.be.calledTwice
|
||||||
@ -148,10 +154,10 @@ describe 'DeviceConfig', ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
promise = Promise.try =>
|
promise = Promise.try =>
|
||||||
@deviceConfig.bootConfigChangeRequired('jetson-tx2', current, target)
|
@deviceConfig.bootConfigChangeRequired(extlinuxBackend, current, target)
|
||||||
expect(promise).to.eventually.equal(true)
|
expect(promise).to.eventually.equal(true)
|
||||||
promise.then =>
|
promise.then =>
|
||||||
@deviceConfig.setBootConfig('jetson-tx2', target)
|
@deviceConfig.setBootConfig(extlinuxBackend, target)
|
||||||
.then =>
|
.then =>
|
||||||
expect(childProcess.execAsync).to.be.calledOnce
|
expect(childProcess.execAsync).to.be.calledOnce
|
||||||
expect(@fakeLogger.logSystemMessage).to.be.calledTwice
|
expect(@fakeLogger.logSystemMessage).to.be.calledTwice
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
m = require 'mochainon'
|
m = require 'mochainon'
|
||||||
{ expect } = m.chai
|
{ expect } = m.chai
|
||||||
|
{ stub } = m.sinon
|
||||||
|
|
||||||
|
{ fs } = require 'mz'
|
||||||
|
|
||||||
configUtils = require '../src/lib/config-utils'
|
configUtils = require '../src/lib/config-utils'
|
||||||
|
{ ExtlinuxConfigBackend, RPiConfigBackend } = require '../src/lib/config-backend'
|
||||||
|
|
||||||
|
extlinuxBackend = new ExtlinuxConfigBackend()
|
||||||
|
rpiBackend = new RPiConfigBackend()
|
||||||
|
|
||||||
describe 'Config Utilities', ->
|
describe 'Config Utilities', ->
|
||||||
|
|
||||||
@ -10,7 +17,7 @@ describe 'Config Utilities', ->
|
|||||||
describe 'Env <-> Config', ->
|
describe 'Env <-> Config', ->
|
||||||
|
|
||||||
it 'correctly transforms environments to boot config objects', ->
|
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_initramfs: 'initramf.gz 0x00800000'
|
||||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||||
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
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
|
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('DEFAULT').that.equals('primary')
|
||||||
expect(parsed.globals).to.have.property('TIMEOUT').that.equals('30')
|
expect(parsed.globals).to.have.property('TIMEOUT').that.equals('30')
|
||||||
expect(parsed.globals).to.have.property('MENU TITLE').that.equals('Boot Options')
|
expect(parsed.globals).to.have.property('MENU TITLE').that.equals('Boot Options')
|
||||||
@ -64,7 +71,7 @@ describe 'Config Utilities', ->
|
|||||||
APPEND test4
|
APPEND test4
|
||||||
'''
|
'''
|
||||||
|
|
||||||
parsed = configUtils.parseExtlinuxFile(text)
|
parsed = ExtlinuxConfigBackend.parseExtlinuxFile(text)
|
||||||
expect(parsed.labels).to.have.property('primary').that.deep.equals({
|
expect(parsed.labels).to.have.property('primary').that.deep.equals({
|
||||||
LINUX: 'test1'
|
LINUX: 'test1'
|
||||||
APPEND: 'test2'
|
APPEND: 'test2'
|
||||||
@ -87,9 +94,11 @@ describe 'Config Utilities', ->
|
|||||||
APPEND ${cbootargs} ${resin_kernel_root} ro rootwait isolcpus=3
|
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 = '''
|
text = '''
|
||||||
@ -103,7 +112,10 @@ describe 'Config Utilities', ->
|
|||||||
LINUX /Image
|
LINUX /Image
|
||||||
APPEND ${cbootargs} ${resin_kernel_root} ro rootwait isolcpus=3,4,5
|
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')
|
||||||
|
Reference in New Issue
Block a user