mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-26 17:01:06 +00:00
Support setting device/fleet configuration in extra_uEnv.txt
Closes: #1385 Change-Type: minor Signed-off-by: Miguel Casqueira <miguel@balena.io>
This commit is contained in:
parent
e97faad77b
commit
cac2e3612c
97
package-lock.json
generated
97
package-lock.json
generated
@ -186,6 +186,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -3812,6 +3818,14 @@
|
|||||||
"lodash": "^4.0.0",
|
"lodash": "^4.0.0",
|
||||||
"request": "^2.65.0",
|
"request": "^2.65.0",
|
||||||
"semver": "^5.3.0"
|
"semver": "^5.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"docker-toolbelt": {
|
"docker-toolbelt": {
|
||||||
@ -5370,6 +5384,14 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"pify": "^4.0.1",
|
"pify": "^4.0.1",
|
||||||
"semver": "^5.6.0"
|
"semver": "^5.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pify": {
|
"pify": {
|
||||||
@ -5499,6 +5521,14 @@
|
|||||||
"schema-utils": "1.0.0",
|
"schema-utils": "1.0.0",
|
||||||
"semver": "^5.6.0",
|
"semver": "^5.6.0",
|
||||||
"tapable": "^1.0.0"
|
"tapable": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form-data": {
|
"form-data": {
|
||||||
@ -8340,6 +8370,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"semver": "^5.4.1"
|
"semver": "^5.4.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node-libs-browser": {
|
"node-libs-browser": {
|
||||||
@ -8417,6 +8455,11 @@
|
|||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -8720,6 +8763,14 @@
|
|||||||
"semver": "^5.5.0",
|
"semver": "^5.5.0",
|
||||||
"shebang-command": "^1.2.0",
|
"shebang-command": "^1.2.0",
|
||||||
"which": "^1.2.9"
|
"which": "^1.2.9"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"execa": {
|
"execa": {
|
||||||
@ -8817,6 +8868,14 @@
|
|||||||
"registry-auth-token": "^3.0.1",
|
"registry-auth-token": "^3.0.1",
|
||||||
"registry-url": "^3.0.3",
|
"registry-url": "^3.0.3",
|
||||||
"semver": "^5.1.0"
|
"semver": "^5.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pako": {
|
"pako": {
|
||||||
@ -9878,9 +9937,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.6.0",
|
"version": "7.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||||
"integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
|
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
|
||||||
},
|
},
|
||||||
"semver-compare": {
|
"semver-compare": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -9895,6 +9954,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"semver": "^5.0.3"
|
"semver": "^5.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"semver-regex": {
|
"semver-regex": {
|
||||||
@ -11275,6 +11342,12 @@
|
|||||||
"path-parse": "^1.0.6"
|
"path-parse": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "1.13.0",
|
"version": "1.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
|
||||||
@ -11875,6 +11948,7 @@
|
|||||||
"anymatch": "^2.0.0",
|
"anymatch": "^2.0.0",
|
||||||
"async-each": "^1.0.1",
|
"async-each": "^1.0.1",
|
||||||
"braces": "^2.3.2",
|
"braces": "^2.3.2",
|
||||||
|
"fsevents": "^1.2.7",
|
||||||
"glob-parent": "^3.1.0",
|
"glob-parent": "^3.1.0",
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"is-binary-path": "^1.0.0",
|
"is-binary-path": "^1.0.0",
|
||||||
@ -11885,6 +11959,16 @@
|
|||||||
"upath": "^1.1.1"
|
"upath": "^1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fsevents": {
|
||||||
|
"version": "1.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
|
||||||
|
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"nan": "^2.12.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"glob-parent": {
|
"glob-parent": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
|
||||||
@ -12070,6 +12154,13 @@
|
|||||||
"semver": "^5.5.0",
|
"semver": "^5.5.0",
|
||||||
"shebang-command": "^1.2.0",
|
"shebang-command": "^1.2.0",
|
||||||
"which": "^1.2.9"
|
"which": "^1.2.9"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"emoji-regex": {
|
"emoji-regex": {
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dbus": "^1.0.7",
|
"dbus": "^1.0.7",
|
||||||
|
"semver": "^7.3.2",
|
||||||
"sqlite3": "^4.1.1"
|
"sqlite3": "^4.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -23,7 +23,7 @@ export async function remountAndWriteAtomic(
|
|||||||
|
|
||||||
export abstract class DeviceConfigBackend {
|
export abstract class DeviceConfigBackend {
|
||||||
// Does this config backend support the given device type?
|
// Does this config backend support the given device type?
|
||||||
public abstract matches(deviceType: string): boolean;
|
public abstract matches(deviceType: string, metaRelease?: string): boolean;
|
||||||
|
|
||||||
// A function which reads and parses the configuration options from
|
// A function which reads and parses the configuration options from
|
||||||
// specific boot config
|
// specific boot config
|
||||||
@ -42,7 +42,7 @@ export abstract class DeviceConfigBackend {
|
|||||||
|
|
||||||
// Convert a configuration environment variable to a config backend
|
// Convert a configuration environment variable to a config backend
|
||||||
// variable
|
// variable
|
||||||
public abstract processConfigVarName(envVar: string): string;
|
public abstract processConfigVarName(envVar: string): string | null;
|
||||||
|
|
||||||
// Process the value if the environment variable, ready to be written to
|
// Process the value if the environment variable, ready to be written to
|
||||||
// the backend
|
// the backend
|
||||||
@ -52,7 +52,10 @@ export abstract class DeviceConfigBackend {
|
|||||||
): string | string[];
|
): string | string[];
|
||||||
|
|
||||||
// Return the env var name for this config option
|
// Return the env var name for this config option
|
||||||
public abstract createConfigVarName(configName: string): string;
|
// In situations when the configName is not valid the backend is unable
|
||||||
|
// to create the varName equivelant so null is returned.
|
||||||
|
// Example an empty string should return null.
|
||||||
|
public abstract createConfigVarName(configName: string): string | null;
|
||||||
|
|
||||||
// Allow a chosen config backend to be initialised
|
// Allow a chosen config backend to be initialised
|
||||||
public async initialise(): Promise<DeviceConfigBackend> {
|
public async initialise(): Promise<DeviceConfigBackend> {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { fs } from 'mz';
|
import { fs } from 'mz';
|
||||||
|
import * as semver from 'semver';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConfigOptions,
|
ConfigOptions,
|
||||||
@ -15,7 +16,7 @@ import {
|
|||||||
} from './extlinux-file';
|
} from './extlinux-file';
|
||||||
import * as constants from '../../lib/constants';
|
import * as constants from '../../lib/constants';
|
||||||
import log from '../../lib/supervisor-console';
|
import log from '../../lib/supervisor-console';
|
||||||
import { ExtLinuxParseError } from '../../lib/errors';
|
import { ExtLinuxEnvError, ExtLinuxParseError } from '../../lib/errors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A backend to handle extlinux host configuration
|
* A backend to handle extlinux host configuration
|
||||||
@ -43,8 +44,13 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
|
|||||||
'(?:' + _.escapeRegExp(ExtlinuxConfigBackend.bootConfigVarPrefix) + ')(.+)',
|
'(?:' + _.escapeRegExp(ExtlinuxConfigBackend.bootConfigVarPrefix) + ')(.+)',
|
||||||
);
|
);
|
||||||
|
|
||||||
public matches(deviceType: string): boolean {
|
public matches(deviceType: string, metaRelease: string | undefined): boolean {
|
||||||
return deviceType.startsWith('jetson-tx');
|
return (
|
||||||
|
// Only test metaRelease with Jetson devices
|
||||||
|
deviceType.startsWith('jetson-') &&
|
||||||
|
typeof metaRelease === 'string' &&
|
||||||
|
semver.lt(metaRelease, constants.extLinuxReadOnly)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBootConfig(): Promise<ConfigOptions> {
|
public async getBootConfig(): Promise<ConfigOptions> {
|
||||||
@ -58,7 +64,7 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
|
|||||||
} catch {
|
} catch {
|
||||||
// In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot
|
// In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot
|
||||||
// We do not have any backup to fallback too; warn the user of a possible brick
|
// We do not have any backup to fallback too; warn the user of a possible brick
|
||||||
throw new ExtLinuxParseError(
|
throw new ExtLinuxEnvError(
|
||||||
'Could not find extlinux file. Device is possibly bricked',
|
'Could not find extlinux file. Device is possibly bricked',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -104,7 +110,7 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
|
|||||||
} catch {
|
} catch {
|
||||||
// In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot
|
// In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot
|
||||||
// We do not have any backup to fallback too; warn the user of a possible brick
|
// We do not have any backup to fallback too; warn the user of a possible brick
|
||||||
throw new Error(
|
throw new ExtLinuxEnvError(
|
||||||
'Could not find extlinux file. Device is possibly bricked',
|
'Could not find extlinux file. Device is possibly bricked',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
272
src/config/backends/extra-uEnv.ts
Normal file
272
src/config/backends/extra-uEnv.ts
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import * as _ from 'lodash';
|
||||||
|
import { fs } from 'mz';
|
||||||
|
import * as semver from 'semver';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConfigOptions,
|
||||||
|
DeviceConfigBackend,
|
||||||
|
bootMountPoint,
|
||||||
|
remountAndWriteAtomic,
|
||||||
|
} from './backend';
|
||||||
|
import * as constants from '../../lib/constants';
|
||||||
|
import log from '../../lib/supervisor-console';
|
||||||
|
import { ExtraUEnvError } from '../../lib/errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry describes the configurable items in an extra_uEnv file
|
||||||
|
*
|
||||||
|
* @collection - This describes if the value can be a list of items seperated by space character.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Entry = {
|
||||||
|
key: EntryKey;
|
||||||
|
collection: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Types of entries supported in a extra_uEnv file
|
||||||
|
type EntryKey = 'custom_fdt_file' | 'extra_os_cmdline';
|
||||||
|
|
||||||
|
// Splits a string from a file into lines
|
||||||
|
const LINE_REGEX = /(?:\r?\n[\s#]*)+/;
|
||||||
|
|
||||||
|
// Splits a line into key value pairs on `=`
|
||||||
|
const OPTION_REGEX = /^\s*(\w+)=(.*)$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A backend to handle host configuration with extra_uEnv
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - {BALENA|RESIN}_HOST_EXTLINUX_isolcpus = value | "value" | "value1","value2"
|
||||||
|
* - {BALENA|RESIN}_HOST_EXTLINUX_fdt = value | "value"
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ExtraUEnvConfigBackend extends DeviceConfigBackend {
|
||||||
|
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}EXTLINUX_`;
|
||||||
|
private static bootConfigPath = `${bootMountPoint}/extra_uEnv.txt`;
|
||||||
|
|
||||||
|
private static entries: Record<EntryKey, Entry> = {
|
||||||
|
custom_fdt_file: { key: 'custom_fdt_file', collection: false },
|
||||||
|
extra_os_cmdline: { key: 'extra_os_cmdline', collection: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
private static supportedConfigs: Dictionary<Entry> = {
|
||||||
|
fdt: ExtraUEnvConfigBackend.entries['custom_fdt_file'],
|
||||||
|
isolcpus: ExtraUEnvConfigBackend.entries['extra_os_cmdline'],
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bootConfigVarRegex = new RegExp(
|
||||||
|
'(?:' +
|
||||||
|
_.escapeRegExp(ExtraUEnvConfigBackend.bootConfigVarPrefix) +
|
||||||
|
')(.+)',
|
||||||
|
);
|
||||||
|
|
||||||
|
public matches(deviceType: string, metaRelease: string | undefined): boolean {
|
||||||
|
return (
|
||||||
|
deviceType === 'intel-nuc' ||
|
||||||
|
// Test metaRelease for Jetson devices
|
||||||
|
(deviceType.startsWith('jetson') &&
|
||||||
|
// Assume metaRelease is greater than or equal to EXTRA_SUPPORT if undefined
|
||||||
|
(typeof metaRelease === 'undefined' ||
|
||||||
|
semver.gte(metaRelease, constants.extLinuxReadOnly)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBootConfig(): Promise<ConfigOptions> {
|
||||||
|
// Get config contents at bootConfigPath
|
||||||
|
const confContents = await ExtraUEnvConfigBackend.readBootConfigPath();
|
||||||
|
|
||||||
|
// Parse ConfigOptions from bootConfigPath contents
|
||||||
|
const parsedConfigFile = ExtraUEnvConfigBackend.parseOptions(confContents);
|
||||||
|
|
||||||
|
// Filter out unsupported values
|
||||||
|
return _.pickBy(parsedConfigFile, (_value, key) =>
|
||||||
|
this.isSupportedConfig(key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setBootConfig(opts: ConfigOptions): Promise<void> {
|
||||||
|
// Filter out unsupported options
|
||||||
|
const supportedOptions = _.pickBy(opts, (value, key) => {
|
||||||
|
if (!this.isSupportedConfig(key)) {
|
||||||
|
log.warn(`Not setting unsupported value: { ${key}: ${value} }`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write new extra_uEnv configuration
|
||||||
|
return await remountAndWriteAtomic(
|
||||||
|
ExtraUEnvConfigBackend.bootConfigPath,
|
||||||
|
ExtraUEnvConfigBackend.configToString(supportedOptions),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSupportedConfig(config: string): boolean {
|
||||||
|
return config in ExtraUEnvConfigBackend.supportedConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isBootConfigVar(envVar: string): boolean {
|
||||||
|
return envVar.startsWith(ExtraUEnvConfigBackend.bootConfigVarPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public processConfigVarName(envVar: string): string | null {
|
||||||
|
const name = envVar.replace(
|
||||||
|
ExtraUEnvConfigBackend.bootConfigVarRegex,
|
||||||
|
'$1',
|
||||||
|
);
|
||||||
|
if (name === envVar) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public processConfigVarValue(_key: string, value: string): string {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public createConfigVarName(configName: string): string | null {
|
||||||
|
if (configName === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${ExtraUEnvConfigBackend.bootConfigVarPrefix}${configName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseOptions(configFile: string): ConfigOptions {
|
||||||
|
// Exit early if configFile is empty
|
||||||
|
if (configFile.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// Split by line and filter any comments and empty lines
|
||||||
|
const lines = configFile.split(LINE_REGEX);
|
||||||
|
// Reduce lines to ConfigOptions
|
||||||
|
return lines.reduce((options: ConfigOptions, line: string) => {
|
||||||
|
const optionValues = line.match(OPTION_REGEX);
|
||||||
|
if (optionValues == null) {
|
||||||
|
log.warn(`Could not read extra_uEnv entry: ${line}`);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
// Merge new option with existing options
|
||||||
|
return {
|
||||||
|
...ExtraUEnvConfigBackend.parseOption(optionValues),
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Return split collection of options
|
||||||
|
return ExtraUEnvConfigBackend.parseOptionCollection(VALUE);
|
||||||
|
}
|
||||||
|
// Find the option that belongs to this entry
|
||||||
|
const optionKey = _.findKey(
|
||||||
|
ExtraUEnvConfigBackend.supportedConfigs,
|
||||||
|
(config) => config.key === KEY,
|
||||||
|
);
|
||||||
|
// Check if we found a corresponding option for this entry
|
||||||
|
if (typeof optionKey !== 'string') {
|
||||||
|
log.warn(`Could not parse unsupported option: ${optionArray[0]}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return { [optionKey]: VALUE };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseOptionCollection(
|
||||||
|
collectionString: string,
|
||||||
|
): ConfigOptions {
|
||||||
|
return (
|
||||||
|
collectionString
|
||||||
|
// Split collection into individual options
|
||||||
|
.split(' ')
|
||||||
|
// Reduce list of option strings into ConfigOptions object
|
||||||
|
.reduce((options: ConfigOptions, option: string) => {
|
||||||
|
// Match optionValues to key=value regex
|
||||||
|
const optionValues = option.match(OPTION_REGEX);
|
||||||
|
// Check if option is only a key
|
||||||
|
if (optionValues === null) {
|
||||||
|
if (option !== '') {
|
||||||
|
return { [option]: '', ...options };
|
||||||
|
} else {
|
||||||
|
log.warn(`Unable to set empty value option: ${option}`);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [, KEY, VALUE] = optionValues;
|
||||||
|
// Merge new option with existing options
|
||||||
|
return { [KEY]: VALUE, ...options };
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async readBootConfigPath(): Promise<string> {
|
||||||
|
try {
|
||||||
|
return await fs.readFile(ExtraUEnvConfigBackend.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}`,
|
||||||
|
);
|
||||||
|
throw new ExtraUEnvError(
|
||||||
|
'Could not find extra_uEnv file. Device is possibly bricked',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static configToString(configs: ConfigOptions): string {
|
||||||
|
// Get Map of ConfigOptions object
|
||||||
|
const configMap = ExtraUEnvConfigBackend.configToMap(configs);
|
||||||
|
// Iterator over configMap and concat to configString
|
||||||
|
let configString = '';
|
||||||
|
for (const [key, value] of configMap) {
|
||||||
|
// Append new config
|
||||||
|
configString += `${key}=${value}\n`;
|
||||||
|
}
|
||||||
|
return configString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static configToMap(configs: ConfigOptions): Map<string, string> {
|
||||||
|
// Reduce ConfigOptions into a Map that joins collections
|
||||||
|
return Object.entries(configs).reduce(
|
||||||
|
(configMap: Map<string, string>, [configKey, configValue]) => {
|
||||||
|
const {
|
||||||
|
key: ENTRY_KEY,
|
||||||
|
collection: ENTRY_IS_COLLECTION,
|
||||||
|
} = ExtraUEnvConfigBackend.supportedConfigs[configKey];
|
||||||
|
// Check if we have to build the value for the entry
|
||||||
|
if (ENTRY_IS_COLLECTION) {
|
||||||
|
return configMap.set(
|
||||||
|
ENTRY_KEY,
|
||||||
|
ExtraUEnvConfigBackend.appendToCollection(
|
||||||
|
configMap.get(ENTRY_KEY),
|
||||||
|
configKey,
|
||||||
|
configValue,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Set the value of this config
|
||||||
|
return configMap.set(ENTRY_KEY, `${configValue}`);
|
||||||
|
},
|
||||||
|
// Start with empty Map
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static appendToCollection(
|
||||||
|
collection: string = '',
|
||||||
|
key: string,
|
||||||
|
value: string | string[],
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
// Start with existing collection and add a space
|
||||||
|
(collection !== '' ? `${collection} ` : '') +
|
||||||
|
// Append new key
|
||||||
|
key +
|
||||||
|
// Append value it's if not empty string
|
||||||
|
(value !== '' ? `=${value}` : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,37 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
import * as constants from '../lib/constants';
|
||||||
|
import { getMetaOSRelease } from '../lib/os-release';
|
||||||
import { EnvVarObject } from '../lib/types';
|
import { EnvVarObject } from '../lib/types';
|
||||||
import { ExtlinuxConfigBackend } from './backends/extlinux';
|
import { ExtlinuxConfigBackend } from './backends/extlinux';
|
||||||
|
import { ExtraUEnvConfigBackend } from './backends/extra-uEnv';
|
||||||
import { RPiConfigBackend } from './backends/raspberry-pi';
|
import { RPiConfigBackend } from './backends/raspberry-pi';
|
||||||
import { ConfigfsConfigBackend } from './backends/config-fs';
|
import { ConfigfsConfigBackend } from './backends/config-fs';
|
||||||
import { ConfigOptions, DeviceConfigBackend } from './backends/backend';
|
import { ConfigOptions, DeviceConfigBackend } from './backends/backend';
|
||||||
|
|
||||||
const configBackends = [
|
const configBackends = [
|
||||||
new ExtlinuxConfigBackend(),
|
new ExtlinuxConfigBackend(),
|
||||||
|
new ExtraUEnvConfigBackend(),
|
||||||
new RPiConfigBackend(),
|
new RPiConfigBackend(),
|
||||||
new ConfigfsConfigBackend(),
|
new ConfigfsConfigBackend(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const initialiseConfigBackend = async (deviceType: string) => {
|
export const initialiseConfigBackend = async (deviceType: string) => {
|
||||||
const backend = getConfigBackend(deviceType);
|
const backend = await getConfigBackend(deviceType);
|
||||||
if (backend) {
|
if (backend) {
|
||||||
await backend.initialise();
|
await backend.initialise();
|
||||||
return backend;
|
return backend;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getConfigBackend(deviceType: string): DeviceConfigBackend | undefined {
|
async function getConfigBackend(
|
||||||
return _.find(configBackends, (backend) => backend.matches(deviceType));
|
deviceType: string,
|
||||||
|
): Promise<DeviceConfigBackend | undefined> {
|
||||||
|
// Some backends are only supported by certain release versions so pass in metaRelease
|
||||||
|
const metaRelease = await getMetaOSRelease(constants.hostOSVersionPath);
|
||||||
|
return _.find(configBackends, (backend) =>
|
||||||
|
backend.matches(deviceType, metaRelease),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function envToBootConfig(
|
export function envToBootConfig(
|
||||||
|
@ -65,6 +65,8 @@ const constants = {
|
|||||||
// (this number is used as an upper bound when generating
|
// (this number is used as an upper bound when generating
|
||||||
// a random jitter)
|
// a random jitter)
|
||||||
maxApiJitterDelay: 60 * 1000,
|
maxApiJitterDelay: 60 * 1000,
|
||||||
|
// The OS version when extlinux moved to READ ONLY partition
|
||||||
|
extLinuxReadOnly: '2.47.0',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.DOCKER_HOST == null) {
|
if (process.env.DOCKER_HOST == null) {
|
||||||
|
@ -107,6 +107,29 @@ export class AppsJsonParseError extends TypedError {}
|
|||||||
export class DatabaseParseError extends TypedError {}
|
export class DatabaseParseError extends TypedError {}
|
||||||
export class BackupError extends TypedError {}
|
export class BackupError extends TypedError {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown if we cannot parse an extlinux file.
|
||||||
|
*/
|
||||||
export class ExtLinuxParseError extends TypedError {}
|
export class ExtLinuxParseError extends TypedError {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown if there is a problem with the environment of which extlinux config is in.
|
||||||
|
* This can be things like missing config files or config files we cannot write to.
|
||||||
|
*/
|
||||||
|
export class ExtLinuxEnvError extends TypedError {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown if we cannot parse the APPEND directive from a extlinux file
|
||||||
|
*/
|
||||||
export class AppendDirectiveError extends TypedError {}
|
export class AppendDirectiveError extends TypedError {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown if we cannot parse the FDT directive from a extlinux file
|
||||||
|
*/
|
||||||
export class FDTDirectiveError extends TypedError {}
|
export class FDTDirectiveError extends TypedError {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic error thrown when something goes wrong with handling the ExtraUEnv backend.
|
||||||
|
* This can be things like missing config files or config files we cannot write to.
|
||||||
|
*/
|
||||||
|
export class ExtraUEnvError extends TypedError {}
|
||||||
|
@ -59,6 +59,12 @@ export function getOSSemver(path: string): Promise<string | undefined> {
|
|||||||
return getOSReleaseField(path, 'VERSION');
|
return getOSReleaseField(path, 'VERSION');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMetaOSRelease(
|
||||||
|
path: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
return getOSReleaseField(path, 'META_BALENA_VERSION');
|
||||||
|
}
|
||||||
|
|
||||||
const L4T_REGEX = /^.*-l4t-r(\d+\.\d+(\.?\d+)?).*$/;
|
const L4T_REGEX = /^.*-l4t-r(\d+\.\d+(\.?\d+)?).*$/;
|
||||||
export async function getL4tVersion(): Promise<string | undefined> {
|
export async function getL4tVersion(): Promise<string | undefined> {
|
||||||
// We call `uname -r` on the host, and look for l4t
|
// We call `uname -r` on the host, and look for l4t
|
||||||
|
@ -123,13 +123,8 @@ describe('Extlinux Configuration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('only matches supported devices', () => {
|
it('only matches supported devices', () => {
|
||||||
[
|
MATCH_TESTS.forEach(({ deviceType, metaRelease, supported }) =>
|
||||||
{ deviceType: 'jetson-tx', supported: true },
|
expect(backend.matches(deviceType, metaRelease)).to.equal(supported),
|
||||||
{ deviceType: 'raspberry', supported: false },
|
|
||||||
{ deviceType: 'fincm3', supported: false },
|
|
||||||
{ deviceType: 'up-board', supported: false },
|
|
||||||
].forEach(({ deviceType, supported }) =>
|
|
||||||
expect(backend.matches(deviceType)).to.equal(supported),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -324,3 +319,84 @@ const MALFORMED_CONFIGS = [
|
|||||||
reason: 'Unable to parse invalid value: isolcpus=0,4=woops',
|
reason: 'Unable to parse invalid value: isolcpus=0,4=woops',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const SUPPORTED_VERSION = '2.45.0'; // or less
|
||||||
|
const UNSUPPORTED_VERSION = '2.47.0'; // or greater
|
||||||
|
|
||||||
|
const MATCH_TESTS = [
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-tx1',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-tx2',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-tx2',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-nano',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-nano',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-xavier',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-xavier',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'intel-nuc',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'intel-nuc',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'raspberry',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'raspberry',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'fincm3',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'fincm3',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'up-board',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'up-board',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
317
test/31-extra-uenv-config.spec.ts
Normal file
317
test/31-extra-uenv-config.spec.ts
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import { child_process, fs } from 'mz';
|
||||||
|
import { stripIndent } from 'common-tags';
|
||||||
|
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';
|
||||||
|
|
||||||
|
describe('extra_uEnv Configuration', () => {
|
||||||
|
const backend = new ExtraUEnvConfigBackend();
|
||||||
|
let readFileStub: SinonStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
readFileStub = stub(fs, 'readFile');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
readFileStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse extra_uEnv string', () => {
|
||||||
|
const fileContents = stripIndent`\
|
||||||
|
custom_fdt_file=mycustom.dtb
|
||||||
|
extra_os_cmdline=isolcpus=3,4 splash console=tty0
|
||||||
|
`;
|
||||||
|
// @ts-ignore accessing private method
|
||||||
|
const parsed = ExtraUEnvConfigBackend.parseOptions(fileContents);
|
||||||
|
expect(parsed).to.deep.equal({
|
||||||
|
fdt: 'mycustom.dtb',
|
||||||
|
isolcpus: '3,4',
|
||||||
|
splash: '',
|
||||||
|
console: 'tty0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only parse supported configuration options from bootConfigPath', async () => {
|
||||||
|
readFileStub.resolves(stripIndent`\
|
||||||
|
custom_fdt_file=mycustom.dtb
|
||||||
|
extra_os_cmdline=isolcpus=3,4
|
||||||
|
`);
|
||||||
|
|
||||||
|
await expect(backend.getBootConfig()).to.eventually.deep.equal({
|
||||||
|
fdt: 'mycustom.dtb',
|
||||||
|
isolcpus: '3,4',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add other options that will get filtered out because they aren't supported
|
||||||
|
readFileStub.resolves(stripIndent`\
|
||||||
|
custom_fdt_file=mycustom.dtb
|
||||||
|
extra_os_cmdline=isolcpus=3,4 console=tty0 splash
|
||||||
|
`);
|
||||||
|
|
||||||
|
await expect(backend.getBootConfig()).to.eventually.deep.equal({
|
||||||
|
fdt: 'mycustom.dtb',
|
||||||
|
isolcpus: '3,4',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stub with no supported values
|
||||||
|
readFileStub.resolves(stripIndent`\
|
||||||
|
fdt=something_else
|
||||||
|
isolcpus
|
||||||
|
123.12=5
|
||||||
|
`);
|
||||||
|
|
||||||
|
await expect(backend.getBootConfig()).to.eventually.deep.equal({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only matches supported devices', () => {
|
||||||
|
MATCH_TESTS.forEach(({ deviceType, metaRelease, supported }) =>
|
||||||
|
expect(backend.matches(deviceType, metaRelease)).to.equal(supported),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors when cannot find extra_uEnv.txt', async () => {
|
||||||
|
// Stub readFile to reject much like if the file didn't exist
|
||||||
|
readFileStub.rejects();
|
||||||
|
await expect(backend.getBootConfig()).to.eventually.be.rejectedWith(
|
||||||
|
'Could not find extra_uEnv file. Device is possibly bricked',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs warning for malformed extra_uEnv.txt', async () => {
|
||||||
|
spy(Log, 'warn');
|
||||||
|
for (const badConfig of MALFORMED_CONFIGS) {
|
||||||
|
// Stub bad config
|
||||||
|
readFileStub.resolves(badConfig.contents);
|
||||||
|
// Expect warning log from the given bad config
|
||||||
|
await backend.getBootConfig();
|
||||||
|
// @ts-ignore
|
||||||
|
expect(Log.warn.lastCall?.lastArg).to.equal(badConfig.reason);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
Log.warn.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets new config values', async () => {
|
||||||
|
stub(fsUtils, 'writeFileAtomic').resolves();
|
||||||
|
stub(child_process, 'exec').resolves();
|
||||||
|
const logWarningStub = spy(Log, 'warn');
|
||||||
|
|
||||||
|
// This config contains a value set from something else
|
||||||
|
// We to make sure the Supervisor is enforcing the source of truth (the cloud)
|
||||||
|
// So after setting new values this unsupported/not set value should be gone
|
||||||
|
readFileStub.resolves(stripIndent`\
|
||||||
|
extra_os_cmdline=rootwait isolcpus=3,4
|
||||||
|
other_service=set_this_value
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Sets config with mix of supported and not supported values
|
||||||
|
await backend.setBootConfig({
|
||||||
|
fdt: '/boot/mycustomdtb.dtb',
|
||||||
|
isolcpus: '2',
|
||||||
|
console: 'tty0', // not supported so won't be set
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fsUtils.writeFileAtomic).to.be.calledWith(
|
||||||
|
'./test/data/mnt/boot/extra_uEnv.txt',
|
||||||
|
'custom_fdt_file=/boot/mycustomdtb.dtb\nextra_os_cmdline=isolcpus=2\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logWarningStub.lastCall?.lastArg).to.equal(
|
||||||
|
'Not setting unsupported value: { console: tty0 }',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore stubs
|
||||||
|
(fsUtils.writeFileAtomic as SinonStub).restore();
|
||||||
|
(child_process.exec as SinonStub).restore();
|
||||||
|
logWarningStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets new config values containing collections', async () => {
|
||||||
|
stub(fsUtils, 'writeFileAtomic').resolves();
|
||||||
|
stub(child_process, 'exec').resolves();
|
||||||
|
const logWarningStub = spy(Log, 'warn');
|
||||||
|
|
||||||
|
// @ts-ignore accessing private value
|
||||||
|
const previousSupportedConfigs = ExtraUEnvConfigBackend.supportedConfigs;
|
||||||
|
// Stub isSupportedConfig so we can confirm collections work
|
||||||
|
// @ts-ignore accessing private value
|
||||||
|
ExtraUEnvConfigBackend.supportedConfigs = {
|
||||||
|
fdt: { key: 'custom_fdt_file', collection: false },
|
||||||
|
isolcpus: { key: 'extra_os_cmdline', collection: true },
|
||||||
|
console: { key: 'extra_os_cmdline', collection: true },
|
||||||
|
splash: { key: 'extra_os_cmdline', collection: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set config again
|
||||||
|
await backend.setBootConfig({
|
||||||
|
fdt: '/boot/mycustomdtb.dtb',
|
||||||
|
isolcpus: '2', // collection entry so should be concatted to other collections of this entry
|
||||||
|
console: 'tty0', // collection entry so should be concatted to other collections of this entry
|
||||||
|
splash: '', // collection entry so should be concatted to other collections of this entry
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fsUtils.writeFileAtomic).to.be.calledWith(
|
||||||
|
'./test/data/mnt/boot/extra_uEnv.txt',
|
||||||
|
'custom_fdt_file=/boot/mycustomdtb.dtb\nextra_os_cmdline=isolcpus=2 console=tty0 splash\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore stubs
|
||||||
|
(fsUtils.writeFileAtomic as SinonStub).restore();
|
||||||
|
(child_process.exec as SinonStub).restore();
|
||||||
|
logWarningStub.restore();
|
||||||
|
// @ts-ignore accessing private value
|
||||||
|
ExtraUEnvConfigBackend.supportedConfigs = previousSupportedConfigs;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only allows supported configuration options', () => {
|
||||||
|
[
|
||||||
|
{ configName: 'fdt', supported: true },
|
||||||
|
{ configName: 'isolcpus', supported: true },
|
||||||
|
{ configName: 'custom_fdt_file', supported: false },
|
||||||
|
{ configName: 'splash', supported: false },
|
||||||
|
{ configName: '', supported: false },
|
||||||
|
].forEach(({ configName, supported }) =>
|
||||||
|
expect(backend.isSupportedConfig(configName)).to.equal(supported),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly detects boot config variables', () => {
|
||||||
|
[
|
||||||
|
{ config: 'HOST_EXTLINUX_isolcpus', valid: true },
|
||||||
|
{ config: 'HOST_EXTLINUX_fdt', valid: true },
|
||||||
|
{ config: 'HOST_EXTLINUX_rootwait', valid: true },
|
||||||
|
{ config: 'HOST_EXTLINUX_5', valid: true },
|
||||||
|
{ config: 'DEVICE_EXTLINUX_isolcpus', valid: false },
|
||||||
|
{ config: 'isolcpus', valid: false },
|
||||||
|
].forEach(({ config, valid }) =>
|
||||||
|
expect(backend.isBootConfigVar(config)).to.equal(valid),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts variable to backend formatted name', () => {
|
||||||
|
[
|
||||||
|
{ input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' },
|
||||||
|
{ input: 'HOST_EXTLINUX_fdt', output: 'fdt' },
|
||||||
|
{ input: 'HOST_EXTLINUX_', output: null },
|
||||||
|
{ input: 'value', output: null },
|
||||||
|
].forEach(({ input, output }) =>
|
||||||
|
expect(backend.processConfigVarName(input)).to.equal(output),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes variable value', () => {
|
||||||
|
[
|
||||||
|
{ input: { key: 'key', value: 'value' }, output: 'value' },
|
||||||
|
].forEach(({ input, output }) =>
|
||||||
|
expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
|
||||||
|
output,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the environment name for config variable', () => {
|
||||||
|
[
|
||||||
|
{ input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' },
|
||||||
|
{ input: 'fdt', output: 'HOST_EXTLINUX_fdt' },
|
||||||
|
{ input: 'rootwait', output: 'HOST_EXTLINUX_rootwait' },
|
||||||
|
{ input: '', output: null },
|
||||||
|
].forEach(({ input, output }) =>
|
||||||
|
expect(backend.createConfigVarName(input)).to.equal(output),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const MALFORMED_CONFIGS = [
|
||||||
|
{
|
||||||
|
contents: stripIndent`
|
||||||
|
custom_fdt_file=mycustom.dtb
|
||||||
|
extra_os_cmdline=isolcpus=3,4
|
||||||
|
another_value
|
||||||
|
`,
|
||||||
|
reason: 'Could not read extra_uEnv entry: another_value',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const SUPPORTED_VERSION = '2.47.0'; // or greater
|
||||||
|
const UNSUPPORTED_VERSION = '2.45.0'; // or less
|
||||||
|
|
||||||
|
const MATCH_TESTS = [
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-tx1',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-tx2',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-tx2',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-nano',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-nano',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-xavier',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'jetson-xavier',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'intel-nuc',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'intel-nuc',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'raspberry',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'raspberry',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'fincm3',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'fincm3',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'up-board',
|
||||||
|
metaRelease: SUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceType: 'up-board',
|
||||||
|
metaRelease: UNSUPPORTED_VERSION,
|
||||||
|
supported: false,
|
||||||
|
},
|
||||||
|
];
|
35
test/32-os-release.spec.ts
Normal file
35
test/32-os-release.spec.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
import * as osRelease from '../src/lib/os-release';
|
||||||
|
|
||||||
|
const OS_RELEASE_PATH = 'test/data/etc/os-release-tx2';
|
||||||
|
|
||||||
|
describe('OS Release Information', () => {
|
||||||
|
it('gets pretty name', async () => {
|
||||||
|
// Try to get PRETTY_NAME
|
||||||
|
await expect(osRelease.getOSVersion(OS_RELEASE_PATH)).to.eventually.equal(
|
||||||
|
'balenaOS 2.45.1+rev3',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets variant', async () => {
|
||||||
|
// Try to get VARIANT_ID
|
||||||
|
await expect(osRelease.getOSVariant(OS_RELEASE_PATH)).to.eventually.equal(
|
||||||
|
'prod',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets version', async () => {
|
||||||
|
// Try to get VERSION
|
||||||
|
await expect(osRelease.getOSSemver(OS_RELEASE_PATH)).to.eventually.equal(
|
||||||
|
'2.45.1+rev3',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets meta release version', async () => {
|
||||||
|
// Try to get META_BALENA_VERSIONS
|
||||||
|
await expect(
|
||||||
|
osRelease.getMetaOSRelease(OS_RELEASE_PATH),
|
||||||
|
).to.eventually.equal('2.45.1');
|
||||||
|
});
|
||||||
|
});
|
12
test/data/etc/os-release-tx2
Normal file
12
test/data/etc/os-release-tx2
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
ID="balena-os"
|
||||||
|
NAME="balenaOS"
|
||||||
|
VERSION="2.45.1+rev3"
|
||||||
|
VERSION_ID="2.45.1+rev3"
|
||||||
|
PRETTY_NAME="balenaOS 2.45.1+rev3"
|
||||||
|
MACHINE="jetson-tx2"
|
||||||
|
VARIANT="Production"
|
||||||
|
VARIANT_ID="prod"
|
||||||
|
META_BALENA_VERSION="2.45.1"
|
||||||
|
RESIN_BOARD_REV="13bd883"
|
||||||
|
META_RESIN_REV="0c90c7e"
|
||||||
|
SLUG="jetson-tx2"
|
Loading…
Reference in New Issue
Block a user