Merge pull request #1144 from balena-io/config-txt-fix

Create empty config.txt in case there isn't one present
This commit is contained in:
Theodor Gherzan 2019-11-19 10:10:46 +00:00 committed by GitHub
commit 3dc53fd2b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 155 deletions

View File

@ -1,12 +1,8 @@
import * as Promise from 'bluebird';
import * as childProcessSync from 'child_process';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { fs } from 'mz'; import { child_process, fs } from 'mz';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import * as fsUtils from '../lib/fs-utils'; import { writeFileAtomic } from '../lib/fs-utils';
const childProcess: any = Promise.promisifyAll(childProcessSync);
import log from '../lib/supervisor-console'; import log from '../lib/supervisor-console';
@ -25,18 +21,15 @@ interface ExtlinuxFile {
const bootMountPoint = `${constants.rootMountPoint}${constants.bootMountPoint}`; const bootMountPoint = `${constants.rootMountPoint}${constants.bootMountPoint}`;
function remountAndWriteAtomic(file: string, data: string): Promise<void> { async function remountAndWriteAtomic(
// TODO: Find out why the below Promise.resolve() is required file: string,
data: string,
): Promise<void> {
// Here's the dangerous part: // Here's the dangerous part:
return Promise.resolve( await child_process.exec(
childProcess.execAsync( `mount -t vfat -o remount,rw ${constants.bootBlockDevice} ${bootMountPoint}`,
`mount -t vfat -o remount,rw ${constants.bootBlockDevice} ${bootMountPoint}`, );
), await writeFileAtomic(file, data);
)
.then(() => {
return fsUtils.writeFileAtomic(file, data);
})
.return();
} }
export abstract class DeviceConfigBackend { export abstract class DeviceConfigBackend {
@ -106,54 +99,61 @@ export class RPiConfigBackend extends DeviceConfigBackend {
return _.startsWith(deviceType, 'raspberry') || deviceType === 'fincm3'; return _.startsWith(deviceType, 'raspberry') || deviceType === 'fincm3';
} }
public getBootConfig(): Promise<ConfigOptions> { public async getBootConfig(): Promise<ConfigOptions> {
return Promise.resolve( let configContents = '';
fs.readFile(RPiConfigBackend.bootConfigPath, 'utf-8'),
).then(confStr => {
const conf: ConfigOptions = {};
const configStatements = confStr.split(/\r?\n/);
for (const configStr of configStatements) { if (await fs.exists(RPiConfigBackend.bootConfigPath)) {
// Don't show warnings for comments and empty lines configContents = await fs.readFile(
const trimmed = _.trimStart(configStr); RPiConfigBackend.bootConfigPath,
if (_.startsWith(trimmed, '#') || trimmed === '') { 'utf-8',
continue; );
} } else {
let keyValue = /^([^=]+)=(.*)$/.exec(configStr); await fs.writeFile(RPiConfigBackend.bootConfigPath, '');
if (keyValue != null) { }
const [, key, value] = keyValue;
if (!_.includes(RPiConfigBackend.arrayConfigKeys, key)) {
conf[key] = value;
} else {
if (conf[key] == null) {
conf[key] = [];
}
const confArr = conf[key];
if (!_.isArray(confArr)) {
throw new Error(
`Expected '${key}' to have a config array but got ${typeof confArr}`,
);
}
confArr.push(value);
}
continue;
}
// Try the next regex instead const conf: ConfigOptions = {};
keyValue = /^(initramfs) (.+)/.exec(configStr); const configStatements = configContents.split(/\r?\n/);
if (keyValue != null) {
const [, key, value] = keyValue; 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; conf[key] = value;
} else { } else {
log.warn(`Could not parse config.txt entry: ${configStr}. Ignoring.`); if (conf[key] == null) {
conf[key] = [];
}
const confArr = conf[key];
if (!_.isArray(confArr)) {
throw new Error(
`Expected '${key}' to have a config array but got ${typeof confArr}`,
);
}
confArr.push(value);
} }
continue;
} }
return conf; // Try the next regex instead
}); keyValue = /^(initramfs) (.+)/.exec(configStr);
if (keyValue != null) {
const [, key, value] = keyValue;
conf[key] = value;
} else {
log.warn(`Could not parse config.txt entry: ${configStr}. Ignoring.`);
}
}
return conf;
} }
public setBootConfig(opts: ConfigOptions): Promise<void> { public async setBootConfig(opts: ConfigOptions): Promise<void> {
let confStatements: string[] = []; let confStatements: string[] = [];
_.each(opts, (value, key) => { _.each(opts, (value, key) => {
@ -170,7 +170,7 @@ export class RPiConfigBackend extends DeviceConfigBackend {
const confStr = `${confStatements.join('\n')}\n`; const confStr = `${confStatements.join('\n')}\n`;
return remountAndWriteAtomic(RPiConfigBackend.bootConfigPath, confStr); await remountAndWriteAtomic(RPiConfigBackend.bootConfigPath, confStr);
} }
public isSupportedConfig(configName: string): boolean { public isSupportedConfig(configName: string): boolean {
@ -215,107 +215,131 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
return _.startsWith(deviceType, 'jetson-tx'); return _.startsWith(deviceType, 'jetson-tx');
} }
public getBootConfig(): Promise<ConfigOptions> { public async getBootConfig(): Promise<ConfigOptions> {
return Promise.resolve( let confContents: string;
fs.readFile(ExtlinuxConfigBackend.bootConfigPath, 'utf-8'),
).then(confStr => {
const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile(confStr);
// First find the default label name try {
const defaultLabel = _.find(parsedBootFile.globals, (_v, l) => { confContents = await fs.readFile(
if (l === 'DEFAULT') { ExtlinuxConfigBackend.bootConfigPath,
return true; 'utf-8',
} );
return false; } 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
throw new Error(
'Could not find extlinux file. Device is possibly bricked',
);
}
if (defaultLabel == null) { const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile(
throw new Error('Could not find default entry for extlinux.conf file'); confContents,
);
// First find the default label name
const defaultLabel = _.find(parsedBootFile.globals, (_v, l) => {
if (l === 'DEFAULT') {
return true;
} }
return false;
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;
}); });
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> { public async setBootConfig(opts: ConfigOptions): Promise<void> {
// First get a representation of the configuration file, with all balena-supported configuration removed // First get a representation of the configuration file, with all balena-supported configuration removed
return Promise.resolve( let confContents: string;
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) { try {
throw new Error( confContents = await fs.readFile(
`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, ExtlinuxConfigBackend.bootConfigPath,
extlinuxString, '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
throw new Error(
'Could not find extlinux file. Device is possibly bricked',
);
}
const extlinuxFile = ExtlinuxConfigBackend.parseExtlinuxFile(
confContents.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,
);
await remountAndWriteAtomic(
ExtlinuxConfigBackend.bootConfigPath,
extlinuxString,
);
} }
public isSupportedConfig(configName: string): boolean { public isSupportedConfig(configName: string): boolean {

View File

@ -13,7 +13,7 @@ fsUtils = require '../src/lib/fs-utils'
extlinuxBackend = new ExtlinuxConfigBackend() extlinuxBackend = new ExtlinuxConfigBackend()
rpiConfigBackend = new RPiConfigBackend() rpiConfigBackend = new RPiConfigBackend()
childProcess = require 'child_process' { child_process } = require 'mz'
describe 'DeviceConfig', -> describe 'DeviceConfig', ->
before -> before ->
@ -111,7 +111,7 @@ describe 'DeviceConfig', ->
it 'writes the target config.txt', -> it 'writes the target config.txt', ->
stub(fsUtils, 'writeFileAtomic').resolves() stub(fsUtils, 'writeFileAtomic').resolves()
stub(childProcess, 'execAsync').resolves() stub(child_process, 'exec').resolves()
current = { current = {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000' HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
HOST_CONFIG_dtparam: '"i2c=on","audio=on"' HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
@ -131,7 +131,7 @@ describe 'DeviceConfig', ->
promise.then => promise.then =>
@deviceConfig.setBootConfig(rpiConfigBackend, target) @deviceConfig.setBootConfig(rpiConfigBackend, target)
.then => .then =>
expect(childProcess.execAsync).to.be.calledOnce expect(child_process.exec).to.be.calledOnce
expect(@fakeLogger.logSystemMessage).to.be.calledTwice expect(@fakeLogger.logSystemMessage).to.be.calledTwice
expect(@fakeLogger.logSystemMessage.getCall(1).args[2]).to.equal('Apply boot config success') expect(@fakeLogger.logSystemMessage.getCall(1).args[2]).to.equal('Apply boot config success')
expect(fsUtils.writeFileAtomic).to.be.calledWith('./test/data/mnt/boot/config.txt', '\ expect(fsUtils.writeFileAtomic).to.be.calledWith('./test/data/mnt/boot/config.txt', '\
@ -143,7 +143,7 @@ describe 'DeviceConfig', ->
foobaz=bar\n\ foobaz=bar\n\
') ')
fsUtils.writeFileAtomic.restore() fsUtils.writeFileAtomic.restore()
childProcess.execAsync.restore() child_process.exec.restore()
@fakeLogger.logSystemMessage.resetHistory() @fakeLogger.logSystemMessage.resetHistory()
it 'accepts RESIN_ and BALENA_ variables', -> it 'accepts RESIN_ and BALENA_ variables', ->
@ -186,7 +186,7 @@ describe 'DeviceConfig', ->
it 'should correctly write to extlinux.conf files', -> it 'should correctly write to extlinux.conf files', ->
stub(fsUtils, 'writeFileAtomic').resolves() stub(fsUtils, 'writeFileAtomic').resolves()
stub(childProcess, 'execAsync').resolves() stub(child_process, 'exec').resolves()
current = { current = {
} }
@ -200,7 +200,7 @@ describe 'DeviceConfig', ->
promise.then => promise.then =>
@deviceConfig.setBootConfig(extlinuxBackend, target) @deviceConfig.setBootConfig(extlinuxBackend, target)
.then => .then =>
expect(childProcess.execAsync).to.be.calledOnce expect(child_process.exec).to.be.calledOnce
expect(@fakeLogger.logSystemMessage).to.be.calledTwice expect(@fakeLogger.logSystemMessage).to.be.calledTwice
expect(@fakeLogger.logSystemMessage.getCall(1).args[2]).to.equal('Apply boot config success') expect(@fakeLogger.logSystemMessage.getCall(1).args[2]).to.equal('Apply boot config success')
expect(fsUtils.writeFileAtomic).to.be.calledWith('./test/data/mnt/boot/extlinux/extlinux.conf', '\ expect(fsUtils.writeFileAtomic).to.be.calledWith('./test/data/mnt/boot/extlinux/extlinux.conf', '\
@ -213,7 +213,7 @@ describe 'DeviceConfig', ->
APPEND ${cbootargs} ${resin_kernel_root} ro rootwait isolcpus=2\n\ APPEND ${cbootargs} ${resin_kernel_root} ro rootwait isolcpus=2\n\
') ')
fsUtils.writeFileAtomic.restore() fsUtils.writeFileAtomic.restore()
childProcess.execAsync.restore() child_process.exec.restore()
@fakeLogger.logSystemMessage.resetHistory() @fakeLogger.logSystemMessage.resetHistory()
describe 'Balena fin', -> describe 'Balena fin', ->

View File

@ -12,6 +12,7 @@ import LocalModeManager, {
EngineSnapshotRecord, EngineSnapshotRecord,
} from '../src/local-mode'; } from '../src/local-mode';
import Logger from '../src/logger'; import Logger from '../src/logger';
import ShortStackError from './lib/errors';
describe('LocalModeManager', () => { describe('LocalModeManager', () => {
let dbFile: tmp.FileResult; let dbFile: tmp.FileResult;
@ -188,7 +189,7 @@ describe('LocalModeManager', () => {
) => { ) => {
const res = sinon.createStubInstance(c); const res = sinon.createStubInstance(c);
if (removeThrows) { if (removeThrows) {
res.remove.rejects(`test error removing ${type}`); res.remove.rejects(new ShortStackError(`error removing ${type}`));
} else { } else {
res.remove.resolves(); res.remove.resolves();
} }

8
test/lib/errors.ts Normal file
View File

@ -0,0 +1,8 @@
import TypedError = require('typed-error');
export default class ShortStackError extends TypedError {
constructor(err: Error | string = '') {
Error.stackTraceLimit = 1;
super(err);
}
}