Merge pull request #1431 from balena-io/allow-multiple-config-backends

Allow multiple config backends
This commit is contained in:
bulldozer-balena[bot] 2020-08-13 16:03:56 +00:00 committed by GitHub
commit 3794c3f7bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 315 additions and 242 deletions

View File

@ -21,7 +21,7 @@ export async function remountAndWriteAtomic(
await writeFileAtomic(file, data);
}
export abstract class DeviceConfigBackend {
export abstract class ConfigBackend {
// Does this config backend support the given device type?
public abstract async matches(
deviceType: string,
@ -61,7 +61,7 @@ export abstract class DeviceConfigBackend {
public abstract createConfigVarName(configName: string): string | null;
// Allow a chosen config backend to be initialised
public async initialise(): Promise<DeviceConfigBackend> {
public async initialise(): Promise<ConfigBackend> {
return this;
}
}

View File

@ -4,7 +4,7 @@ import * as path from 'path';
import {
ConfigOptions,
DeviceConfigBackend,
ConfigBackend,
bootMountPoint,
remountAndWriteAtomic,
} from './backend';
@ -21,7 +21,7 @@ import log from '../../lib/supervisor-console';
type ConfigfsConfig = Dictionary<string[]>;
export class ConfigfsConfigBackend extends DeviceConfigBackend {
export class ConfigFs extends ConfigBackend {
private readonly SystemAmlFiles = path.join(
constants.rootMountPoint,
'boot/acpi-tables',
@ -129,7 +129,7 @@ export class ConfigfsConfigBackend extends DeviceConfigBackend {
}
}
public async initialise(): Promise<ConfigfsConfigBackend> {
public async initialise(): Promise<ConfigFs> {
try {
await super.initialise();
@ -158,7 +158,7 @@ export class ConfigfsConfigBackend extends DeviceConfigBackend {
}
public async matches(deviceType: string): Promise<boolean> {
return ConfigfsConfigBackend.SupportedDeviceTypes.includes(deviceType);
return ConfigFs.SupportedDeviceTypes.includes(deviceType);
}
public async getBootConfig(): Promise<ConfigOptions> {
@ -195,15 +195,11 @@ export class ConfigfsConfigBackend extends DeviceConfigBackend {
}
public isSupportedConfig(name: string): boolean {
return ConfigfsConfigBackend.BootConfigVars.includes(
this.stripPrefix(name),
);
return ConfigFs.BootConfigVars.includes(this.stripPrefix(name));
}
public isBootConfigVar(name: string): boolean {
return ConfigfsConfigBackend.BootConfigVars.includes(
this.stripPrefix(name),
);
return ConfigFs.BootConfigVars.includes(this.stripPrefix(name));
}
public processConfigVarName(name: string): string {

View File

@ -3,7 +3,7 @@ import { fs } from 'mz';
import {
ConfigOptions,
DeviceConfigBackend,
ConfigBackend,
bootMountPoint,
remountAndWriteAtomic,
} from './backend';
@ -21,12 +21,12 @@ import log from '../../lib/supervisor-console';
* - {BALENA|RESIN}_HOST_CONFIG_gpio = value | "value" | "value1","value2"
*/
export class RPiConfigBackend extends DeviceConfigBackend {
export class ConfigTxt extends ConfigBackend {
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}CONFIG_`;
private static bootConfigPath = `${bootMountPoint}/config.txt`;
public static bootConfigVarRegex = new RegExp(
'(?:' + _.escapeRegExp(RPiConfigBackend.bootConfigVarPrefix) + ')(.+)',
'(?:' + _.escapeRegExp(ConfigTxt.bootConfigVarPrefix) + ')(.+)',
);
private static arrayConfigKeys = [
@ -57,13 +57,10 @@ export class RPiConfigBackend extends DeviceConfigBackend {
public async getBootConfig(): Promise<ConfigOptions> {
let configContents = '';
if (await fs.exists(RPiConfigBackend.bootConfigPath)) {
configContents = await fs.readFile(
RPiConfigBackend.bootConfigPath,
'utf-8',
);
if (await fs.exists(ConfigTxt.bootConfigPath)) {
configContents = await fs.readFile(ConfigTxt.bootConfigPath, 'utf-8');
} else {
await fs.writeFile(RPiConfigBackend.bootConfigPath, '');
await fs.writeFile(ConfigTxt.bootConfigPath, '');
}
const conf: ConfigOptions = {};
@ -78,7 +75,7 @@ export class RPiConfigBackend extends DeviceConfigBackend {
let keyValue = /^([^=]+)=(.*)$/.exec(configStr);
if (keyValue != null) {
const [, key, value] = keyValue;
if (!RPiConfigBackend.arrayConfigKeys.includes(key)) {
if (!ConfigTxt.arrayConfigKeys.includes(key)) {
conf[key] = value;
} else {
if (conf[key] == null) {
@ -119,23 +116,23 @@ export class RPiConfigBackend extends DeviceConfigBackend {
}
});
const confStr = `${confStatements.join('\n')}\n`;
await remountAndWriteAtomic(RPiConfigBackend.bootConfigPath, confStr);
await remountAndWriteAtomic(ConfigTxt.bootConfigPath, confStr);
}
public isSupportedConfig(configName: string): boolean {
return !RPiConfigBackend.forbiddenConfigKeys.includes(configName);
return !ConfigTxt.forbiddenConfigKeys.includes(configName);
}
public isBootConfigVar(envVar: string): boolean {
return envVar.startsWith(RPiConfigBackend.bootConfigVarPrefix);
return envVar.startsWith(ConfigTxt.bootConfigVarPrefix);
}
public processConfigVarName(envVar: string): string {
return envVar.replace(RPiConfigBackend.bootConfigVarRegex, '$1');
return envVar.replace(ConfigTxt.bootConfigVarRegex, '$1');
}
public processConfigVarValue(key: string, value: string): string | string[] {
if (RPiConfigBackend.arrayConfigKeys.includes(key)) {
if (ConfigTxt.arrayConfigKeys.includes(key)) {
if (!value.startsWith('"')) {
return [value];
} else {
@ -146,6 +143,6 @@ export class RPiConfigBackend extends DeviceConfigBackend {
}
public createConfigVarName(configName: string): string {
return RPiConfigBackend.bootConfigVarPrefix + configName;
return ConfigTxt.bootConfigVarPrefix + configName;
}
}

View File

@ -4,7 +4,7 @@ import * as semver from 'semver';
import {
ConfigOptions,
DeviceConfigBackend,
ConfigBackend,
bootMountPoint,
remountAndWriteAtomic,
} from './backend';
@ -29,7 +29,7 @@ const EXTLINUX_READONLY = '2.47.0';
* - {BALENA|RESIN}_HOST_EXTLINUX_fdt = value | "value"
*/
export class ExtlinuxConfigBackend extends DeviceConfigBackend {
export class Extlinux extends ConfigBackend {
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}EXTLINUX_`;
private static bootConfigPath = `${bootMountPoint}/extlinux/extlinux.conf`;
private static supportedConfigValues = ['isolcpus', 'fdt'];
@ -38,13 +38,11 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
private fdtDirective = new FDTDirective();
private appendDirective = new AppendDirective(
// Pass in list of supportedConfigValues that APPEND can have
ExtlinuxConfigBackend.supportedConfigValues.filter(
(v) => !this.isDirective(v),
),
Extlinux.supportedConfigValues.filter((v) => !this.isDirective(v)),
);
public static bootConfigVarRegex = new RegExp(
'(?:' + _.escapeRegExp(ExtlinuxConfigBackend.bootConfigVarPrefix) + ')(.+)',
'(?:' + _.escapeRegExp(Extlinux.bootConfigVarPrefix) + ')(.+)',
);
public async matches(
@ -63,10 +61,7 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
let confContents: string;
try {
confContents = await fs.readFile(
ExtlinuxConfigBackend.bootConfigPath,
'utf-8',
);
confContents = await fs.readFile(Extlinux.bootConfigPath, 'utf-8');
} catch {
// In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot
// We do not have any backup to fallback too; warn the user of a possible brick
@ -76,18 +71,13 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
}
// Parse ExtlinuxFile from file contents
const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile(
confContents,
);
const parsedBootFile = Extlinux.parseExtlinuxFile(confContents);
// Get default label to know which label entry to parse
const defaultLabel = ExtlinuxConfigBackend.findDefaultLabel(parsedBootFile);
const defaultLabel = Extlinux.findDefaultLabel(parsedBootFile);
// Get the label entry we will parse
const labelEntry = ExtlinuxConfigBackend.getLabelEntry(
parsedBootFile,
defaultLabel,
);
const labelEntry = Extlinux.getLabelEntry(parsedBootFile, defaultLabel);
// Parse APPEND directive and filter out unsupported values
const appendConfig = _.pickBy(
@ -109,10 +99,7 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
let confContents: string;
try {
confContents = await fs.readFile(
ExtlinuxConfigBackend.bootConfigPath,
'utf-8',
);
confContents = await fs.readFile(Extlinux.bootConfigPath, 'utf-8');
} catch {
// In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot
// We do not have any backup to fallback too; warn the user of a possible brick
@ -122,18 +109,13 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
}
// Parse ExtlinuxFile from file contents
const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile(
confContents,
);
const parsedBootFile = Extlinux.parseExtlinuxFile(confContents);
// Get default label to know which label entry to edit
const defaultLabel = ExtlinuxConfigBackend.findDefaultLabel(parsedBootFile);
const defaultLabel = Extlinux.findDefaultLabel(parsedBootFile);
// Get the label entry we will edit
const defaultEntry = ExtlinuxConfigBackend.getLabelEntry(
parsedBootFile,
defaultLabel,
);
const defaultEntry = Extlinux.getLabelEntry(parsedBootFile, defaultLabel);
// Set `FDT` directive if a value is provided
if (opts.fdt) {
@ -155,21 +137,21 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
// Write new extlinux configuration
return await remountAndWriteAtomic(
ExtlinuxConfigBackend.bootConfigPath,
ExtlinuxConfigBackend.extlinuxFileToString(parsedBootFile),
Extlinux.bootConfigPath,
Extlinux.extlinuxFileToString(parsedBootFile),
);
}
public isSupportedConfig(configName: string): boolean {
return ExtlinuxConfigBackend.supportedConfigValues.includes(configName);
return Extlinux.supportedConfigValues.includes(configName);
}
public isBootConfigVar(envVar: string): boolean {
return envVar.startsWith(ExtlinuxConfigBackend.bootConfigVarPrefix);
return envVar.startsWith(Extlinux.bootConfigVarPrefix);
}
public processConfigVarName(envVar: string): string {
return envVar.replace(ExtlinuxConfigBackend.bootConfigVarRegex, '$1');
return envVar.replace(Extlinux.bootConfigVarRegex, '$1');
}
public processConfigVarValue(_key: string, value: string): string {
@ -177,13 +159,11 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
}
public createConfigVarName(configName: string): string {
return `${ExtlinuxConfigBackend.bootConfigVarPrefix}${configName}`;
return `${Extlinux.bootConfigVarPrefix}${configName}`;
}
private isDirective(configName: string): boolean {
return ExtlinuxConfigBackend.supportedDirectives.includes(
configName.toUpperCase(),
);
return Extlinux.supportedDirectives.includes(configName.toUpperCase());
}
private static parseExtlinuxFile(confStr: string): ExtlinuxFile {

View File

@ -3,7 +3,7 @@ import { fs } from 'mz';
import {
ConfigOptions,
DeviceConfigBackend,
ConfigBackend,
bootMountPoint,
remountAndWriteAtomic,
} from './backend';
@ -40,7 +40,7 @@ const OPTION_REGEX = /^\s*(\w+)=(.*)$/;
* - {BALENA|RESIN}_HOST_EXTLINUX_fdt = value | "value"
*/
export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
export class ExtraUEnv extends ConfigBackend {
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}EXTLINUX_`;
private static bootConfigPath = `${bootMountPoint}/extra_uEnv.txt`;
@ -50,29 +50,27 @@ export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
};
private static supportedConfigs: Dictionary<Entry> = {
fdt: ExtraUEnvConfigBackend.entries['custom_fdt_file'],
isolcpus: ExtraUEnvConfigBackend.entries['extra_os_cmdline'],
fdt: ExtraUEnv.entries['custom_fdt_file'],
isolcpus: ExtraUEnv.entries['extra_os_cmdline'],
};
public static bootConfigVarRegex = new RegExp(
'(?:' +
_.escapeRegExp(ExtraUEnvConfigBackend.bootConfigVarPrefix) +
')(.+)',
'(?:' + _.escapeRegExp(ExtraUEnv.bootConfigVarPrefix) + ')(.+)',
);
public async matches(deviceType: string): Promise<boolean> {
return (
(deviceType === 'intel-nuc' || deviceType.startsWith('jetson')) &&
(await fs.exists(ExtraUEnvConfigBackend.bootConfigPath))
(await fs.exists(ExtraUEnv.bootConfigPath))
);
}
public async getBootConfig(): Promise<ConfigOptions> {
// Get config contents at bootConfigPath
const confContents = await ExtraUEnvConfigBackend.readBootConfigPath();
const confContents = await ExtraUEnv.readBootConfigPath();
// Parse ConfigOptions from bootConfigPath contents
const parsedConfigFile = ExtraUEnvConfigBackend.parseOptions(confContents);
const parsedConfigFile = ExtraUEnv.parseOptions(confContents);
// Filter out unsupported values
return _.pickBy(parsedConfigFile, (_value, key) =>
@ -92,24 +90,21 @@ export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
// Write new extra_uEnv configuration
return await remountAndWriteAtomic(
ExtraUEnvConfigBackend.bootConfigPath,
ExtraUEnvConfigBackend.configToString(supportedOptions),
ExtraUEnv.bootConfigPath,
ExtraUEnv.configToString(supportedOptions),
);
}
public isSupportedConfig(config: string): boolean {
return config in ExtraUEnvConfigBackend.supportedConfigs;
return config in ExtraUEnv.supportedConfigs;
}
public isBootConfigVar(envVar: string): boolean {
return envVar.startsWith(ExtraUEnvConfigBackend.bootConfigVarPrefix);
return envVar.startsWith(ExtraUEnv.bootConfigVarPrefix);
}
public processConfigVarName(envVar: string): string | null {
const name = envVar.replace(
ExtraUEnvConfigBackend.bootConfigVarRegex,
'$1',
);
const name = envVar.replace(ExtraUEnv.bootConfigVarRegex, '$1');
if (name === envVar) {
return null;
}
@ -124,7 +119,7 @@ export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
if (configName === '') {
return null;
}
return `${ExtraUEnvConfigBackend.bootConfigVarPrefix}${configName}`;
return `${ExtraUEnv.bootConfigVarPrefix}${configName}`;
}
private static parseOptions(configFile: string): ConfigOptions {
@ -143,7 +138,7 @@ export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
}
// Merge new option with existing options
return {
...ExtraUEnvConfigBackend.parseOption(optionValues),
...ExtraUEnv.parseOption(optionValues),
...options,
};
}, {});
@ -152,13 +147,13 @@ export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
private static parseOption(optionArray: string[]): ConfigOptions {
const [, KEY, VALUE] = optionArray;
// Check if this key's value is a collection
if (ExtraUEnvConfigBackend.entries[KEY as EntryKey]?.collection) {
if (ExtraUEnv.entries[KEY as EntryKey]?.collection) {
// Return split collection of options
return ExtraUEnvConfigBackend.parseOptionCollection(VALUE);
return ExtraUEnv.parseOptionCollection(VALUE);
}
// Find the option that belongs to this entry
const optionKey = _.findKey(
ExtraUEnvConfigBackend.supportedConfigs,
ExtraUEnv.supportedConfigs,
(config) => config.key === KEY,
);
// Check if we found a corresponding option for this entry
@ -198,12 +193,12 @@ export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
private static async readBootConfigPath(): Promise<string> {
try {
return await fs.readFile(ExtraUEnvConfigBackend.bootConfigPath, 'utf-8');
return await fs.readFile(ExtraUEnv.bootConfigPath, 'utf-8');
} catch {
// In the rare case where the user might have deleted extra_uEnv conf file between linux boot and supervisor boot
// We do not have any backup to fallback too; warn the user of a possible brick
log.error(
`Unable to read extra_uEnv file at: ${ExtraUEnvConfigBackend.bootConfigPath}`,
`Unable to read extra_uEnv file at: ${ExtraUEnv.bootConfigPath}`,
);
throw new ExtraUEnvError(
'Could not find extra_uEnv file. Device is possibly bricked',
@ -213,7 +208,7 @@ export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
private static configToString(configs: ConfigOptions): string {
// Get Map of ConfigOptions object
const configMap = ExtraUEnvConfigBackend.configToMap(configs);
const configMap = ExtraUEnv.configToMap(configs);
// Iterator over configMap and concat to configString
let configString = '';
for (const [key, value] of configMap) {
@ -230,12 +225,12 @@ export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
const {
key: ENTRY_KEY,
collection: ENTRY_IS_COLLECTION,
} = ExtraUEnvConfigBackend.supportedConfigs[configKey];
} = ExtraUEnv.supportedConfigs[configKey];
// Check if we have to build the value for the entry
if (ENTRY_IS_COLLECTION) {
return configMap.set(
ENTRY_KEY,
ExtraUEnvConfigBackend.appendToCollection(
ExtraUEnv.appendToCollection(
configMap.get(ENTRY_KEY),
configKey,
configValue,

View File

@ -0,0 +1,11 @@
import { Extlinux } from './extlinux';
import { ExtraUEnv } from './extra-uEnv';
import { ConfigTxt } from './config-txt';
import { ConfigFs } from './config-fs';
export default [
new Extlinux(),
new ExtraUEnv(),
new ConfigTxt(),
new ConfigFs(),
];

View File

@ -1,51 +1,32 @@
import * as _ from 'lodash';
import * as Bluebird from 'bluebird';
import * as config from '../config';
import * as constants from '../lib/constants';
import { getMetaOSRelease } from '../lib/os-release';
import { EnvVarObject } from '../lib/types';
import { ExtlinuxConfigBackend } from './backends/extlinux';
import { ExtraUEnvConfigBackend } from './backends/extra-uEnv';
import { RPiConfigBackend } from './backends/raspberry-pi';
import { ConfigfsConfigBackend } from './backends/config-fs';
import { ConfigOptions, DeviceConfigBackend } from './backends/backend';
import Backends from './backends';
import { ConfigOptions, ConfigBackend } from './backends/backend';
const configBackends = [
new ExtlinuxConfigBackend(),
new ExtraUEnvConfigBackend(),
new RPiConfigBackend(),
new ConfigfsConfigBackend(),
];
export const initialiseConfigBackend = async (deviceType: string) => {
const backend = await getConfigBackend(deviceType);
if (backend) {
await backend.initialise();
return backend;
}
};
async function getConfigBackend(
deviceType: string,
): Promise<DeviceConfigBackend | undefined> {
// Some backends are only supported by certain release versions so pass in metaRelease
const metaRelease = await getMetaOSRelease(constants.hostOSVersionPath);
let matched;
for (const backend of configBackends) {
if (await backend.matches(deviceType, metaRelease)) {
matched = backend;
}
}
return matched;
export async function getSupportedBackends(): Promise<ConfigBackend[]> {
// Get required information to find supported backends
const [deviceType, metaRelease] = await Promise.all([
config.get('deviceType'),
getMetaOSRelease(constants.hostOSVersionPath),
]);
// Return list of configurable backends that match this deviceType and metaRelease
return Bluebird.filter(Backends, (backend: ConfigBackend) =>
backend.matches(deviceType, metaRelease),
);
}
export function envToBootConfig(
configBackend: DeviceConfigBackend | null,
configBackend: ConfigBackend | null,
env: EnvVarObject,
): ConfigOptions {
if (configBackend == null) {
return {};
}
return _(env)
.pickBy((_val, key) => configBackend.isBootConfigVar(key))
.mapKeys((_val, key) => configBackend.processConfigVarName(key))
@ -56,10 +37,10 @@ export function envToBootConfig(
}
export function bootConfigToEnv(
configBackend: DeviceConfigBackend,
config: ConfigOptions,
configBackend: ConfigBackend,
configOptions: ConfigOptions,
): EnvVarObject {
return _(config)
return _(configOptions)
.mapKeys((_val, key) => configBackend.createConfigVarName(key))
.mapValues((val) => {
if (_.isArray(val)) {
@ -70,22 +51,8 @@ export function bootConfigToEnv(
.value();
}
function filterNamespaceFromConfig(
namespace: RegExp,
conf: { [key: string]: any },
): { [key: string]: any } {
return _.mapKeys(
_.pickBy(conf, (_v, k) => {
return namespace.test(k);
}),
(_v, k) => {
return k.replace(namespace, '$1');
},
);
}
export function formatConfigKeys(
configBackend: DeviceConfigBackend | null,
configBackend: ConfigBackend | null,
allowedKeys: string[],
conf: { [key: string]: any },
): { [key: string]: any } {
@ -113,3 +80,17 @@ export function formatConfigKeys(
);
});
}
function filterNamespaceFromConfig(
namespace: RegExp,
conf: { [key: string]: any },
): { [key: string]: any } {
return _.mapKeys(
_.pickBy(conf, (_v, k) => {
return namespace.test(k);
}),
(_v, k) => {
return k.replace(namespace, '$1');
},
);
}

View File

@ -6,7 +6,7 @@ import { SchemaTypeKey } from './config/schema-type';
import * as db from './db';
import * as logger from './logger';
import { ConfigOptions, DeviceConfigBackend } from './config/backends/backend';
import { ConfigOptions, ConfigBackend } from './config/backends/backend';
import * as configUtils from './config/utils';
import * as dbus from './lib/dbus';
import { UnitNotLoadedError } from './lib/errors';
@ -100,15 +100,17 @@ const actionExecutors: DeviceActionExecutors = {
}
},
setBootConfig: async (step) => {
const $configBackend = await getConfigBackend();
if (!_.isObject(step.target)) {
throw new Error('Non-dictionary passed to DeviceConfig.setBootConfig');
}
await setBootConfig($configBackend, step.target as Dictionary<string>);
const backends = await getConfigBackends();
for (const backend of backends) {
await setBootConfig(backend, step.target as Dictionary<string>);
}
},
};
let configBackend: DeviceConfigBackend | null = null;
const configBackends: ConfigBackend[] = [];
const configKeys: Dictionary<ConfigOption> = {
appUpdatePollInterval: {
@ -206,14 +208,19 @@ const rateLimits: Dictionary<{
},
};
async function getConfigBackend() {
if (configBackend != null) {
return configBackend;
async function getConfigBackends(): Promise<ConfigBackend[]> {
// Exit early if we already have a list
if (configBackends.length > 0) {
return configBackends;
}
const dt = await config.get('deviceType');
configBackend = (await configUtils.initialiseConfigBackend(dt)) ?? null;
return configBackend;
// Get all the configurable backends this device supports
const backends = await configUtils.getSupportedBackends();
// Initialize each backend
for (const backend of backends) {
await backend.initialise();
}
// Return list of initialized ConfigBackends
return backends;
}
export async function setTarget(
@ -264,37 +271,45 @@ export async function getTarget({
return conf;
}
export async function getCurrent() {
export async function getCurrent(): Promise<Dictionary<string>> {
// Build a Dictionary of currently set config values
const currentConf: Dictionary<string> = {};
// Get environment variables
const conf = await config.getMany(
['deviceType'].concat(_.keys(configKeys)) as SchemaTypeKey[],
);
const $configBackend = await getConfigBackend();
const [vpnStatus, bootConfig] = await Promise.all([
getVPNEnabled(),
getBootConfig($configBackend),
]);
const currentConf: Dictionary<string> = {
// TODO: Fix this mess of half strings half boolean values everywhere
SUPERVISOR_VPN_CONTROL: vpnStatus != null ? vpnStatus.toString() : 'true',
};
// Add each value
for (const key of _.keys(configKeys)) {
const { envVarName } = configKeys[key];
const confValue = conf[key as SchemaTypeKey];
currentConf[envVarName] = confValue != null ? confValue.toString() : '';
}
return _.assign(currentConf, bootConfig);
// Add VPN information
currentConf['SUPERVISOR_VPN_CONTROL'] = (await isVPNEnabled())
? 'true'
: 'false';
// Get list of configurable backends
const backends = await getConfigBackends();
// Add each backends configurable values
for (const backend of backends) {
_.assign(currentConf, await getBootConfig(backend));
}
// Return compiled configuration
return currentConf;
}
export async function formatConfigKeys(
conf: Dictionary<string>,
): Promise<Dictionary<any>> {
const backend = await getConfigBackend();
return configUtils.formatConfigKeys(backend, validKeys, conf);
const backends = await getConfigBackends();
const formattedKeys: Dictionary<any> = {};
for (const backend of backends) {
_.assign(
formattedKeys,
configUtils.formatConfigKeys(backend, validKeys, conf),
);
}
return formattedKeys;
}
export function getDefaults() {
@ -314,16 +329,13 @@ export function resetRateLimits() {
// Exported for tests
export function bootConfigChangeRequired(
$configBackend: DeviceConfigBackend | null,
configBackend: ConfigBackend | null,
current: Dictionary<string>,
target: Dictionary<string>,
deviceType: string,
): boolean {
const targetBootConfig = configUtils.envToBootConfig($configBackend, target);
const currentBootConfig = configUtils.envToBootConfig(
$configBackend,
current,
);
const targetBootConfig = configUtils.envToBootConfig(configBackend, target);
const currentBootConfig = configUtils.envToBootConfig(configBackend, current);
// Some devices require specific overlays, here we apply them
ensureRequiredOverlay(deviceType, targetBootConfig);
@ -331,7 +343,7 @@ export function bootConfigChangeRequired(
if (!_.isEqual(currentBootConfig, targetBootConfig)) {
_.each(targetBootConfig, (value, key) => {
// Ignore null check because we can't get here if configBackend is null
if (!$configBackend!.isSupportedConfig(key)) {
if (!configBackend!.isSupportedConfig(key)) {
if (currentBootConfig[key] !== value) {
const err = `Attempt to change blacklisted config value ${key}`;
logger.logSystemMessage(
@ -369,7 +381,6 @@ export async function getRequiredSteps(
'deviceType',
'unmanaged',
]);
const backend = await getConfigBackend();
const configChanges: Dictionary<string> = {};
const humanReadableConfigChanges: Dictionary<string> = {};
@ -455,13 +466,16 @@ export async function getRequiredSteps(
return step;
});
// Do we need to change the boot config?
if (bootConfigChangeRequired(backend, current, target, deviceType)) {
steps.push({
action: 'setBootConfig',
target,
});
}
const backends = await getConfigBackends();
// Check for required bootConfig changes
backends.forEach((backend) => {
if (bootConfigChangeRequired(backend, current, target, deviceType)) {
steps.push({
action: 'setBootConfig',
target,
});
}
});
// Check if there is either no steps, or they are all
// noops, and we need to reboot. We want to do this
@ -492,7 +506,7 @@ export function isValidAction(action: string): boolean {
}
export async function getBootConfig(
backend: DeviceConfigBackend | null,
backend: ConfigBackend | null,
): Promise<EnvVarObject> {
if (backend == null) {
return {};
@ -503,7 +517,7 @@ export async function getBootConfig(
// Exported for tests
export async function setBootConfig(
backend: DeviceConfigBackend | null,
backend: ConfigBackend | null,
target: Dictionary<string>,
) {
if (backend == null) {
@ -539,7 +553,7 @@ export async function setBootConfig(
}
}
async function getVPNEnabled(): Promise<boolean> {
async function isVPNEnabled(): Promise<boolean> {
try {
const activeState = await dbus.serviceActiveState(vpnServiceName);
return !_.includes(['inactive', 'deactivating'], activeState);

View File

@ -10,7 +10,7 @@ import Log from '../src/lib/supervisor-console';
import * as dockerUtils from '../src/lib/docker-utils';
import * as config from '../src/config';
import * as images from '../src/compose/images';
import { RPiConfigBackend } from '../src/config/backends/raspberry-pi';
import { ConfigTxt } from '../src/config/backends/config-txt';
import DeviceState from '../src/device-state';
import * as deviceConfig from '../src/device-config';
import { loadTargetFromFile } from '../src/device-state/preload';
@ -254,7 +254,7 @@ describe('deviceState', () => {
};
// @ts-expect-error Assigning to a RO property
deviceConfig.configBackend = new RPiConfigBackend();
deviceConfig.configBackend = new ConfigTxt();
// @ts-expect-error Assigning to a RO property
deviceConfig.getCurrent = async () => mockedInitialConfig;

View File

@ -6,12 +6,12 @@ import { expect } from './lib/chai-config';
import * as deviceConfig from '../src/device-config';
import * as fsUtils from '../src/lib/fs-utils';
import * as logger from '../src/logger';
import { ExtlinuxConfigBackend } from '../src/config/backends/extlinux';
import { RPiConfigBackend } from '../src/config/backends/raspberry-pi';
import { Extlinux } from '../src/config/backends/extlinux';
import { ConfigTxt } from '../src/config/backends/config-txt';
import prepare = require('./lib/prepare');
const extlinuxBackend = new ExtlinuxConfigBackend();
const rpiConfigBackend = new RPiConfigBackend();
const extlinuxBackend = new Extlinux();
const configTxtBackend = new ConfigTxt();
describe('Device Backend Config', () => {
let logSpy: SinonSpy;
@ -33,7 +33,7 @@ describe('Device Backend Config', () => {
// Will try to parse /test/data/mnt/boot/config.txt
await expect(
// @ts-ignore accessing private value
deviceConfig.getBootConfig(rpiConfigBackend),
deviceConfig.getBootConfig(configTxtBackend),
).to.eventually.deep.equal({
HOST_CONFIG_dtparam: '"i2c_arm=on","spi=on","audio=on"',
HOST_CONFIG_enable_uart: '1',
@ -54,7 +54,7 @@ describe('Device Backend Config', () => {
await expect(
// @ts-ignore accessing private value
deviceConfig.getBootConfig(rpiConfigBackend),
deviceConfig.getBootConfig(configTxtBackend),
).to.eventually.deep.equal({
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
@ -83,7 +83,7 @@ describe('Device Backend Config', () => {
expect(() =>
// @ts-ignore accessing private value
deviceConfig.bootConfigChangeRequired(rpiConfigBackend, current, target),
deviceConfig.bootConfigChangeRequired(configTxtBackend, current, target),
).to.throw('Attempt to change blacklisted config value initramfs');
// Check if logs were called
@ -115,7 +115,7 @@ describe('Device Backend Config', () => {
expect(
// @ts-ignore accessing private value
deviceConfig.bootConfigChangeRequired(rpiConfigBackend, current, target),
deviceConfig.bootConfigChangeRequired(configTxtBackend, current, target),
).to.equal(false);
expect(logSpy).to.not.be.called;
});
@ -140,11 +140,11 @@ describe('Device Backend Config', () => {
expect(
// @ts-ignore accessing private value
deviceConfig.bootConfigChangeRequired(rpiConfigBackend, current, target),
deviceConfig.bootConfigChangeRequired(configTxtBackend, current, target),
).to.equal(true);
// @ts-ignore accessing private value
await deviceConfig.setBootConfig(rpiConfigBackend, target);
await deviceConfig.setBootConfig(configTxtBackend, target);
expect(child_process.exec).to.be.calledOnce;
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
@ -280,7 +280,7 @@ describe('Device Backend Config', () => {
it('should not cause a config change when the cloud does not specify the balena-fin overlay', () => {
expect(
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
configTxtBackend,
{ HOST_CONFIG_dtoverlay: '"test","balena-fin"' },
{ HOST_CONFIG_dtoverlay: '"test"' },
'fincm3',
@ -289,7 +289,7 @@ describe('Device Backend Config', () => {
expect(
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
configTxtBackend,
{ HOST_CONFIG_dtoverlay: '"test","balena-fin"' },
{ HOST_CONFIG_dtoverlay: 'test' },
'fincm3',
@ -298,7 +298,7 @@ describe('Device Backend Config', () => {
expect(
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
configTxtBackend,
{ HOST_CONFIG_dtoverlay: '"test","test2","balena-fin"' },
{ HOST_CONFIG_dtoverlay: '"test","test2"' },
'fincm3',
@ -341,7 +341,7 @@ describe('Device Backend Config', () => {
it('should not cause a config change when the cloud does not specify the pi4 overlay', () => {
expect(
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
configTxtBackend,
{ HOST_CONFIG_dtoverlay: '"test","vc4-fkms-v3d"' },
{ HOST_CONFIG_dtoverlay: '"test"' },
'raspberrypi4-64',
@ -349,7 +349,7 @@ describe('Device Backend Config', () => {
).to.equal(false);
expect(
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
configTxtBackend,
{ HOST_CONFIG_dtoverlay: '"test","vc4-fkms-v3d"' },
{ HOST_CONFIG_dtoverlay: 'test' },
'raspberrypi4-64',
@ -357,7 +357,7 @@ describe('Device Backend Config', () => {
).to.equal(false);
expect(
deviceConfig.bootConfigChangeRequired(
rpiConfigBackend,
configTxtBackend,
{ HOST_CONFIG_dtoverlay: '"test","test2","vc4-fkms-v3d"' },
{ HOST_CONFIG_dtoverlay: '"test","test2"' },
'raspberrypi4-64',
@ -368,7 +368,7 @@ describe('Device Backend Config', () => {
// describe('ConfigFS', () => {
// const upboardConfig = new DeviceConfig();
// let upboardConfigBackend: DeviceConfigBackend | null;
// let upboardConfigBackend: ConfigBackend | null;
// before(async () => {
// stub(child_process, 'exec').resolves();

View File

@ -1,25 +1,124 @@
import { expect } from './lib/chai-config';
import * as configUtils from '../src/config/utils';
import { RPiConfigBackend } from '../src/config/backends/raspberry-pi';
import { stub } from 'sinon';
import * as _ from 'lodash';
const rpiBackend = new RPiConfigBackend();
import { expect } from './lib/chai-config';
import * as config from '../src/config';
import { validKeys } from '../src/device-config';
import * as configUtils from '../src/config/utils';
import { ExtraUEnv } from '../src/config/backends/extra-uEnv';
import { Extlinux } from '../src/config/backends/extlinux';
import { ConfigTxt } from '../src/config/backends/config-txt';
import { ConfigFs } from '../src/config/backends/config-fs';
import { ConfigBackend } from '../src/config/backends/backend';
describe('Config Utilities', () => {
describe('Boot config', () => {
it('correctly transforms environments to boot config objects', () => {
const bootConfig = configUtils.envToBootConfig(rpiBackend, {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
HOST_CONFIG_dtoverlay:
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'baz',
});
expect(bootConfig).to.deep.equal({
initramfs: 'initramf.gz 0x00800000',
dtparam: ['i2c=on', 'audio=on'],
dtoverlay: ['ads7846', 'lirc-rpi,gpio_out_pin=17,gpio_in_pin=13'],
foobar: 'baz',
});
it('gets list of supported backends', async () => {
// Stub so that we get an array containing only config-txt backend
const configStub = stub(config, 'get').resolves('raspberry');
// Get list of backends
const devices = await configUtils.getSupportedBackends();
expect(devices.length).to.equal(1);
expect(devices[0].constructor.name).to.equal('ConfigTxt');
// Restore stub
configStub.restore();
// TO-DO: When we have a device that will match for multiple backends
// add a test that we get more then 1 backend for that device
});
it('transforms environment variables to boot configs', () => {
_.forEach(CONFIGS, (configObj: any, key: string) => {
expect(
configUtils.envToBootConfig(BACKENDS[key], configObj.envVars),
).to.deep.equal(configObj.bootConfig);
});
});
it('transforms boot configs to environment variables', () => {
_.forEach(CONFIGS, (configObj: any, key: string) => {
expect(
configUtils.bootConfigToEnv(BACKENDS[key], configObj.bootConfig),
).to.deep.equal(configObj.envVars);
});
});
it('formats keys from config', () => {
// Pick any backend to use for test
// note: some of the values used will be specific to this backend
const backend = BACKENDS['extlinux'];
const formattedKeys = configUtils.formatConfigKeys(backend, validKeys, {
FOO: 'bar',
BAR: 'baz',
RESIN_HOST_CONFIG_foo: 'foobaz',
BALENA_HOST_CONFIG_foo: 'foobar',
RESIN_HOST_CONFIG_other: 'val',
BALENA_HOST_CONFIG_baz: 'bad',
BALENA_SUPERVISOR_POLL_INTERVAL: '100', // any device
BALENA_HOST_EXTLINUX_isolcpus: '1,2,3', // specific to backend
RESIN_HOST_EXTLINUX_fdt: '/boot/mycustomdtb.dtb', // specific to backend
});
expect(formattedKeys).to.deep.equal({
HOST_EXTLINUX_isolcpus: '1,2,3',
HOST_EXTLINUX_fdt: '/boot/mycustomdtb.dtb',
SUPERVISOR_POLL_INTERVAL: '100',
});
});
});
const BACKENDS: Record<string, ConfigBackend> = {
extraUEnv: new ExtraUEnv(),
extlinux: new Extlinux(),
configtxt: new ConfigTxt(),
configfs: new ConfigFs(),
};
const CONFIGS = {
extraUEnv: {
envVars: {
HOST_EXTLINUX_fdt: '/boot/mycustomdtb.dtb',
HOST_EXTLINUX_isolcpus: '1,2,3',
HOST_EXTLINUX_rootwait: '',
},
bootConfig: {
fdt: '/boot/mycustomdtb.dtb',
isolcpus: '1,2,3',
rootwait: '',
},
},
extlinux: {
envVars: {
HOST_EXTLINUX_fdt: '/boot/mycustomdtb.dtb',
HOST_EXTLINUX_isolcpus: '1,2,3',
HOST_EXTLINUX_rootwait: '',
},
bootConfig: {
fdt: '/boot/mycustomdtb.dtb',
isolcpus: '1,2,3',
rootwait: '',
},
},
configtxt: {
envVars: {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
HOST_CONFIG_dtoverlay:
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'baz',
},
bootConfig: {
initramfs: 'initramf.gz 0x00800000',
dtparam: ['i2c=on', 'audio=on'],
dtoverlay: ['ads7846', 'lirc-rpi,gpio_out_pin=17,gpio_in_pin=13'],
foobar: 'baz',
},
},
// TO-DO: Config-FS is commented out because it behaves differently and doesn't
// add value to the Config Utilities if we make it work but would like to add it
// configfs: {
// envVars: {
// ssdt: 'spidev1,1'
// },
// bootConfig: {
// ssdt: ['spidev1,1']
// },
// },
};

View File

@ -4,10 +4,10 @@ import { SinonStub, stub } from 'sinon';
import { expect } from './lib/chai-config';
import * as fsUtils from '../src/lib/fs-utils';
import { ExtlinuxConfigBackend } from '../src/config/backends/extlinux';
import { Extlinux } from '../src/config/backends/extlinux';
describe('Extlinux Configuration', () => {
const backend = new ExtlinuxConfigBackend();
const backend = new Extlinux();
it('should parse a extlinux.conf file', () => {
const text = stripIndent`\
@ -24,7 +24,7 @@ describe('Extlinux Configuration', () => {
`;
// @ts-ignore accessing private method
const parsed = ExtlinuxConfigBackend.parseExtlinuxFile(text);
const parsed = Extlinux.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)
@ -61,7 +61,7 @@ describe('Extlinux Configuration', () => {
`;
// @ts-ignore accessing private method
const parsed = ExtlinuxConfigBackend.parseExtlinuxFile(text);
const parsed = Extlinux.parseExtlinuxFile(text);
expect(parsed.labels).to.have.property('primary').that.deep.equals({
LINUX: 'test1',
FDT: '/boot/mycustomdtb.dtb',

View File

@ -5,10 +5,10 @@ import { SinonStub, spy, stub } from 'sinon';
import { expect } from './lib/chai-config';
import * as fsUtils from '../src/lib/fs-utils';
import Log from '../src/lib/supervisor-console';
import { ExtraUEnvConfigBackend } from '../src/config/backends/extra-uEnv';
import { ExtraUEnv } from '../src/config/backends/extra-uEnv';
describe('extra_uEnv Configuration', () => {
const backend = new ExtraUEnvConfigBackend();
const backend = new ExtraUEnv();
let readFileStub: SinonStub;
beforeEach(() => {
@ -25,7 +25,7 @@ describe('extra_uEnv Configuration', () => {
extra_os_cmdline=isolcpus=3,4 splash console=tty0
`;
// @ts-ignore accessing private method
const parsed = ExtraUEnvConfigBackend.parseOptions(fileContents);
const parsed = ExtraUEnv.parseOptions(fileContents);
expect(parsed).to.deep.equal({
fdt: 'mycustom.dtb',
isolcpus: '3,4',
@ -138,10 +138,10 @@ describe('extra_uEnv Configuration', () => {
const logWarningStub = spy(Log, 'warn');
// @ts-ignore accessing private value
const previousSupportedConfigs = ExtraUEnvConfigBackend.supportedConfigs;
const previousSupportedConfigs = ExtraUEnv.supportedConfigs;
// Stub isSupportedConfig so we can confirm collections work
// @ts-ignore accessing private value
ExtraUEnvConfigBackend.supportedConfigs = {
ExtraUEnv.supportedConfigs = {
fdt: { key: 'custom_fdt_file', collection: false },
isolcpus: { key: 'extra_os_cmdline', collection: true },
console: { key: 'extra_os_cmdline', collection: true },
@ -166,7 +166,7 @@ describe('extra_uEnv Configuration', () => {
(child_process.exec as SinonStub).restore();
logWarningStub.restore();
// @ts-ignore accessing private value
ExtraUEnvConfigBackend.supportedConfigs = previousSupportedConfigs;
ExtraUEnv.supportedConfigs = previousSupportedConfigs;
});
it('only allows supported configuration options', () => {