mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-28 23:24:01 +00:00
Merge pull request #2379 from balena-os/support-jetson-power-fan-configs
Support jetson power fan configs
This commit is contained in:
commit
e085013548
@ -41,6 +41,12 @@ export abstract class ConfigBackend {
|
||||
// Example an empty string should return null.
|
||||
public abstract createConfigVarName(configName: string): string | null;
|
||||
|
||||
// Is a reboot required for the given config options?
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async isRebootRequired(_opts: ConfigOptions): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow a chosen config backend to be initialised
|
||||
public async initialise(): Promise<ConfigBackend> {
|
||||
return this;
|
||||
|
@ -70,14 +70,15 @@ function isBaseParam(dtparam: string): boolean {
|
||||
* - {BALENA|RESIN}_HOST_CONFIG_gpio = value | "value" | "value1","value2"
|
||||
*/
|
||||
export class ConfigTxt extends ConfigBackend {
|
||||
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}CONFIG_`;
|
||||
private static bootConfigPath = hostUtils.pathOnBoot('config.txt');
|
||||
|
||||
public static bootConfigVarRegex = new RegExp(
|
||||
'(?:' + _.escapeRegExp(ConfigTxt.bootConfigVarPrefix) + ')(.+)',
|
||||
private static PREFIX = `${constants.hostConfigVarPrefix}CONFIG_`;
|
||||
private static PATH = hostUtils.pathOnBoot('config.txt');
|
||||
private static REGEX = new RegExp(
|
||||
'(?:' + _.escapeRegExp(ConfigTxt.PREFIX) + ')(.+)',
|
||||
);
|
||||
|
||||
private static forbiddenConfigKeys = [
|
||||
// These keys are not config.txt keys and are managed by the power-fan backend.
|
||||
private static UNSUPPORTED_KEYS = ['power_mode', 'fan_profile'];
|
||||
// These keys are config.txt keys, but are not mutable by the Supervisor.
|
||||
private static FORBIDDEN_KEYS = [
|
||||
'disable_commandline_tags',
|
||||
'cmdline',
|
||||
'kernel',
|
||||
@ -89,7 +90,7 @@ export class ConfigTxt extends ConfigBackend {
|
||||
'device_tree_address',
|
||||
'init_emmc_clock',
|
||||
'avoid_safe_mode',
|
||||
];
|
||||
].concat(ConfigTxt.UNSUPPORTED_KEYS);
|
||||
|
||||
public async matches(deviceType: string): Promise<boolean> {
|
||||
return (
|
||||
@ -109,11 +110,8 @@ export class ConfigTxt extends ConfigBackend {
|
||||
public async getBootConfig(): Promise<ConfigOptions> {
|
||||
let configContents = '';
|
||||
|
||||
if (await exists(ConfigTxt.bootConfigPath)) {
|
||||
configContents = await hostUtils.readFromBoot(
|
||||
ConfigTxt.bootConfigPath,
|
||||
'utf-8',
|
||||
);
|
||||
if (await exists(ConfigTxt.PATH)) {
|
||||
configContents = await hostUtils.readFromBoot(ConfigTxt.PATH, 'utf-8');
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
@ -227,19 +225,29 @@ export class ConfigTxt extends ConfigBackend {
|
||||
}
|
||||
|
||||
const confStr = `${confStatements.join('\n')}\n`;
|
||||
await hostUtils.writeToBoot(ConfigTxt.bootConfigPath, confStr);
|
||||
await hostUtils.writeToBoot(ConfigTxt.PATH, confStr);
|
||||
}
|
||||
|
||||
public static stripPrefix(name: string): string {
|
||||
if (!name.startsWith(ConfigTxt.PREFIX)) {
|
||||
return name;
|
||||
}
|
||||
return name.substring(ConfigTxt.PREFIX.length);
|
||||
}
|
||||
|
||||
public isSupportedConfig(configName: string): boolean {
|
||||
return !ConfigTxt.forbiddenConfigKeys.includes(configName);
|
||||
return !ConfigTxt.FORBIDDEN_KEYS.includes(configName);
|
||||
}
|
||||
|
||||
public isBootConfigVar(envVar: string): boolean {
|
||||
return envVar.startsWith(ConfigTxt.bootConfigVarPrefix);
|
||||
return (
|
||||
envVar.startsWith(ConfigTxt.PREFIX) &&
|
||||
!ConfigTxt.UNSUPPORTED_KEYS.includes(ConfigTxt.stripPrefix(envVar))
|
||||
);
|
||||
}
|
||||
|
||||
public processConfigVarName(envVar: string): string {
|
||||
return envVar.replace(ConfigTxt.bootConfigVarRegex, '$1');
|
||||
return envVar.replace(ConfigTxt.REGEX, '$1');
|
||||
}
|
||||
|
||||
public processConfigVarValue(key: string, value: string): string | string[] {
|
||||
@ -254,7 +262,7 @@ export class ConfigTxt extends ConfigBackend {
|
||||
}
|
||||
|
||||
public createConfigVarName(configName: string): string {
|
||||
return ConfigTxt.bootConfigVarPrefix + configName;
|
||||
return ConfigTxt.PREFIX + configName;
|
||||
}
|
||||
|
||||
// Ensure that the balena-fin overlay is defined in the target configuration
|
||||
|
@ -4,6 +4,8 @@ import { ConfigTxt } from './config-txt';
|
||||
import { ConfigFs } from './config-fs';
|
||||
import { Odmdata } from './odmdata';
|
||||
import { SplashImage } from './splash-image';
|
||||
import { PowerFanConfig } from './power-fan';
|
||||
import { configJsonBackend } from '..';
|
||||
|
||||
export const allBackends = [
|
||||
new Extlinux(),
|
||||
@ -12,6 +14,7 @@ export const allBackends = [
|
||||
new ConfigFs(),
|
||||
new Odmdata(),
|
||||
new SplashImage(),
|
||||
new PowerFanConfig(configJsonBackend),
|
||||
];
|
||||
|
||||
export function matchesAnyBootConfig(envVar: string): boolean {
|
||||
|
170
src/config/backends/power-fan.ts
Normal file
170
src/config/backends/power-fan.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import Reporter from 'io-ts-reporters';
|
||||
import * as t from 'io-ts';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { ConfigBackend } from './backend';
|
||||
import type { ConfigOptions } from './backend';
|
||||
import { schemaTypes } from '../schema-type';
|
||||
import log from '../../lib/supervisor-console';
|
||||
import * as constants from '../../lib/constants';
|
||||
|
||||
type ConfigJsonBackend = {
|
||||
get: (key: 'os') => Promise<unknown>;
|
||||
set: (opts: { os: Record<string, any> }) => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A backend to handle Jetson power and fan control
|
||||
*
|
||||
* Supports:
|
||||
* - {BALENA|RESIN}_HOST_CONFIG_power_mode = "low" | "mid" | "high" | "default" |"$MODE_ID"
|
||||
* - {BALENA|RESIN}_HOST_CONFIG_fan_profile = "quiet" | "cool" | "default" |"$MODE_ID"
|
||||
*/
|
||||
export class PowerFanConfig extends ConfigBackend {
|
||||
private static readonly CONFIGS = new Set(['power_mode', 'fan_profile']);
|
||||
private static readonly PREFIX = `${constants.hostConfigVarPrefix}CONFIG_`;
|
||||
private static readonly SCHEMA = t.exact(
|
||||
t.partial({
|
||||
power: t.exact(
|
||||
t.partial({
|
||||
mode: t.string,
|
||||
}),
|
||||
),
|
||||
fan: t.exact(
|
||||
t.partial({
|
||||
profile: t.string,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
private readonly configJson: ConfigJsonBackend;
|
||||
public constructor(configJson: ConfigJsonBackend) {
|
||||
super();
|
||||
this.configJson = configJson;
|
||||
}
|
||||
|
||||
public static stripPrefix(name: string): string {
|
||||
if (!name.startsWith(PowerFanConfig.PREFIX)) {
|
||||
return name;
|
||||
}
|
||||
return name.substring(PowerFanConfig.PREFIX.length);
|
||||
}
|
||||
|
||||
public async matches(deviceType: string): Promise<boolean> {
|
||||
// We only support Jetpack 6 devices for now, which includes all Orin devices
|
||||
// except for jetson-orin-nx-xv3 which is still on Jetpack 5 as of OS v5.1.36
|
||||
return new Set([
|
||||
'jetson-agx-orin-devkit',
|
||||
'jetson-agx-orin-devkit-64gb',
|
||||
'jetson-orin-nano-devkit-nvme',
|
||||
'jetson-orin-nano-seeed-j3010',
|
||||
'jetson-orin-nx-seeed-j4012',
|
||||
'jetson-orin-nx-xavier-nx-devkit',
|
||||
]).has(deviceType);
|
||||
}
|
||||
|
||||
public async getBootConfig(): Promise<ConfigOptions> {
|
||||
// Get raw config.json contents
|
||||
let rawConf: unknown;
|
||||
try {
|
||||
rawConf = await this.configJson.get('os');
|
||||
} catch (e: unknown) {
|
||||
log.error(
|
||||
`Failed to read config.json while getting power / fan configs: ${(e as Error).message ?? e}`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Decode to power fan schema from object type, filtering out unrelated values
|
||||
const powerFanConfig = PowerFanConfig.SCHEMA.decode(rawConf);
|
||||
|
||||
if (isRight(powerFanConfig)) {
|
||||
const conf = powerFanConfig.right;
|
||||
return {
|
||||
...(conf.power?.mode != null && {
|
||||
power_mode: conf.power.mode,
|
||||
}),
|
||||
...(conf.fan?.profile != null && {
|
||||
fan_profile: conf.fan.profile,
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
public async setBootConfig(opts: ConfigOptions): Promise<void> {
|
||||
// Read raw configs for "os" key from config.json
|
||||
let rawConf;
|
||||
try {
|
||||
rawConf = await this.configJson.get('os');
|
||||
} catch (err: unknown) {
|
||||
log.error(`${(err as Error).message ?? err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode to "os" object type while leaving in unrelated values
|
||||
const maybeCurrentConf = schemaTypes.os.type.decode(rawConf);
|
||||
if (!isRight(maybeCurrentConf)) {
|
||||
log.error(
|
||||
'Failed to decode current os config:',
|
||||
Reporter.report(maybeCurrentConf),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Current config could be undefined if there's no os key in config.json, so default to empty object
|
||||
const conf = maybeCurrentConf.right ?? {};
|
||||
|
||||
// Update or delete power mode
|
||||
if ('power_mode' in opts) {
|
||||
conf.power = {
|
||||
mode: opts.power_mode,
|
||||
};
|
||||
} else {
|
||||
delete conf?.power;
|
||||
}
|
||||
|
||||
// Update or delete fan profile
|
||||
if ('fan_profile' in opts) {
|
||||
conf.fan = {
|
||||
profile: opts.fan_profile,
|
||||
};
|
||||
} else {
|
||||
delete conf?.fan;
|
||||
}
|
||||
|
||||
await this.configJson.set({ os: conf });
|
||||
}
|
||||
|
||||
public isSupportedConfig = (name: string): boolean => {
|
||||
return PowerFanConfig.CONFIGS.has(PowerFanConfig.stripPrefix(name));
|
||||
};
|
||||
|
||||
public isBootConfigVar(envVar: string): boolean {
|
||||
return PowerFanConfig.CONFIGS.has(PowerFanConfig.stripPrefix(envVar));
|
||||
}
|
||||
|
||||
public async isRebootRequired(opts: ConfigOptions): Promise<boolean> {
|
||||
const supportedOpts = _.pickBy(
|
||||
_.mapKeys(opts, (_value, key) => PowerFanConfig.stripPrefix(key)),
|
||||
(_value, key) => this.isSupportedConfig(key),
|
||||
);
|
||||
const current = await this.getBootConfig();
|
||||
// A reboot is only required if the power mode is changing
|
||||
return current.power_mode !== supportedOpts.power_mode;
|
||||
}
|
||||
|
||||
public processConfigVarName(envVar: string): string {
|
||||
return PowerFanConfig.stripPrefix(envVar).toLowerCase();
|
||||
}
|
||||
|
||||
public processConfigVarValue(_key: string, value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
public createConfigVarName(name: string): string | null {
|
||||
return `${PowerFanConfig.PREFIX}${name}`;
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import _ from 'lodash';
|
||||
|
||||
import * as constants from '../lib/constants';
|
||||
import * as hostUtils from '../lib/host-utils';
|
||||
import * as osRelease from '../lib/os-release';
|
||||
import { takeGlobalLockRO, takeGlobalLockRW } from '../lib/process-lock';
|
||||
import type * as Schema from './schema';
|
||||
|
||||
@ -12,17 +11,20 @@ export default class ConfigJsonConfigBackend {
|
||||
private readonly writeLockConfigJson: () => Bluebird.Disposer<() => void>;
|
||||
|
||||
private readonly schema: Schema.Schema;
|
||||
/**
|
||||
* @deprecated configPath is only set by legacy tests
|
||||
*/
|
||||
private readonly configPath?: string;
|
||||
|
||||
private cache: { [key: string]: unknown } = {};
|
||||
|
||||
private readonly init = _.once(async () =>
|
||||
Object.assign(this.cache, await this.read()),
|
||||
);
|
||||
private readonly init = _.once(async () => {
|
||||
Object.assign(this.cache, await this.read());
|
||||
});
|
||||
|
||||
public constructor(schema: Schema.Schema, configPath?: string) {
|
||||
this.configPath = configPath;
|
||||
this.schema = schema;
|
||||
this.configPath = configPath;
|
||||
|
||||
this.writeLockConfigJson = () =>
|
||||
takeGlobalLockRW('config.json').disposer((release) => release());
|
||||
@ -37,14 +39,10 @@ export default class ConfigJsonConfigBackend {
|
||||
await Bluebird.using(this.writeLockConfigJson(), async () => {
|
||||
let changed = false;
|
||||
_.forOwn(keyVals, (value, key: T) => {
|
||||
if (this.cache[key] !== value) {
|
||||
if (this.schema[key] != null && !_.isEqual(this.cache[key], value)) {
|
||||
this.cache[key] = value;
|
||||
|
||||
if (
|
||||
value == null &&
|
||||
this.schema[key] != null &&
|
||||
this.schema[key].removeIfNull
|
||||
) {
|
||||
if (value == null && this.schema[key].removeIfNull) {
|
||||
delete this.cache[key];
|
||||
}
|
||||
|
||||
@ -57,15 +55,14 @@ export default class ConfigJsonConfigBackend {
|
||||
});
|
||||
}
|
||||
|
||||
public async get(key: string): Promise<unknown> {
|
||||
public async get(key: Schema.SchemaKey): Promise<unknown> {
|
||||
await this.init();
|
||||
return Bluebird.using(
|
||||
this.readLockConfigJson(),
|
||||
async () => this.cache[key],
|
||||
return Bluebird.using(this.readLockConfigJson(), async () =>
|
||||
structuredClone(this.cache[key]),
|
||||
);
|
||||
}
|
||||
|
||||
public async remove(key: string) {
|
||||
public async remove(key: Schema.SchemaKey) {
|
||||
await this.init();
|
||||
return Bluebird.using(this.writeLockConfigJson(), async () => {
|
||||
let changed = false;
|
||||
@ -91,6 +88,12 @@ export default class ConfigJsonConfigBackend {
|
||||
return JSON.parse(await hostUtils.readFromBoot(filename, 'utf-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Either read the config.json path from lib/constants, or
|
||||
* pass a validated path to the constructor and fail if no path is passed.
|
||||
* TODO: Remove this once api-binder tests are migrated. The only
|
||||
* time configPath is passed to the constructor is in the legacy tests.
|
||||
*/
|
||||
private async path(): Promise<string> {
|
||||
// TODO: Remove this once api-binder tests are migrated. The only
|
||||
// time configPath is passed to the constructor is in the legacy tests.
|
||||
@ -98,11 +101,6 @@ export default class ConfigJsonConfigBackend {
|
||||
return this.configPath;
|
||||
}
|
||||
|
||||
const osVersion = await osRelease.getOSVersion(constants.hostOSVersionPath);
|
||||
if (osVersion == null) {
|
||||
throw new Error('Failed to detect OS version!');
|
||||
}
|
||||
|
||||
// The default path in the boot partition
|
||||
return constants.configJsonPath;
|
||||
}
|
||||
|
@ -84,6 +84,10 @@ export const schemaTypes = {
|
||||
type: t.string,
|
||||
default: NullOrUndefined,
|
||||
},
|
||||
os: {
|
||||
type: t.union([t.record(t.string, t.any), t.undefined]),
|
||||
default: NullOrUndefined,
|
||||
},
|
||||
|
||||
// Database types
|
||||
name: {
|
||||
|
@ -84,6 +84,11 @@ export const schema = {
|
||||
mutable: false,
|
||||
removeIfNull: false,
|
||||
},
|
||||
os: {
|
||||
source: 'config.json',
|
||||
mutable: true,
|
||||
removeIfNull: false,
|
||||
},
|
||||
|
||||
name: {
|
||||
source: 'db',
|
||||
|
@ -529,18 +529,21 @@ async function getBackendSteps(
|
||||
const { deviceType } = await config.getMany(['deviceType']);
|
||||
|
||||
// Check for required bootConfig changes
|
||||
let rebootRequired = false;
|
||||
for (const backend of backends) {
|
||||
if (changeRequired(backend, current, target, deviceType)) {
|
||||
steps.push({
|
||||
action: 'setBootConfig',
|
||||
target,
|
||||
});
|
||||
rebootRequired =
|
||||
(await backend.isRebootRequired(target)) || rebootRequired;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
// All backend steps require a reboot
|
||||
...(steps.length > 0
|
||||
// All backend steps require a reboot except fan control
|
||||
...(steps.length > 0 && rebootRequired
|
||||
? [{ action: 'setRebootBreadcrumb' } as ConfigStep]
|
||||
: []),
|
||||
...steps,
|
||||
|
203
test/integration/config/configJson.spec.ts
Normal file
203
test/integration/config/configJson.spec.ts
Normal file
@ -0,0 +1,203 @@
|
||||
import { expect } from 'chai';
|
||||
import type { TestFs } from 'mocha-pod';
|
||||
import { testfs } from 'mocha-pod';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import ConfigJsonConfigBackend from '~/src/config/configJson';
|
||||
import { schema } from '~/src/config/schema';
|
||||
|
||||
describe('ConfigJsonConfigBackend', () => {
|
||||
const CONFIG_PATH = '/mnt/boot/config.json';
|
||||
const os = {
|
||||
power: {
|
||||
mode: 'high',
|
||||
},
|
||||
fan: {
|
||||
profile: 'cool',
|
||||
},
|
||||
network: {
|
||||
connectivity: {
|
||||
uri: 'https://api.balena-cloud.com/connectivity-check',
|
||||
interval: '300',
|
||||
response: 'optional value in the response',
|
||||
},
|
||||
wifi: {
|
||||
randomMacAddressScan: false,
|
||||
},
|
||||
},
|
||||
udevRules: {
|
||||
'56': 'ENV{ID_FS_LABEL_ENC}=="resin-root*", IMPORT{program}="resin_update_state_probe $devnode", SYMLINK+="disk/by-state/$env{RESIN_UPDATE_STATE}"',
|
||||
'64': 'ACTION!="add|change", GOTO="modeswitch_rules_end"\nKERNEL=="ttyACM*", ATTRS{idVendor}=="1546", ATTRS{idProduct}=="1146", TAG+="systemd", ENV{SYSTEMD_WANTS}="u-blox-switch@\'%E{DEVNAME}\'.service"\nLBEL="modeswitch_rules_end"\n',
|
||||
},
|
||||
sshKeys: [
|
||||
'ssh-rsa AAAAB3Nza...M2JB balena@macbook-pro',
|
||||
'ssh-rsa AAAAB3Nza...nFTQ balena@zenbook',
|
||||
],
|
||||
};
|
||||
let configJsonConfigBackend: ConfigJsonConfigBackend;
|
||||
let tfs: TestFs.Enabled;
|
||||
|
||||
beforeEach(() => {
|
||||
configJsonConfigBackend = new ConfigJsonConfigBackend(schema);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await tfs.restore();
|
||||
});
|
||||
|
||||
it('should get primitive values for config.json key', async () => {
|
||||
tfs = await testfs({
|
||||
[CONFIG_PATH]: JSON.stringify({
|
||||
apiEndpoint: 'foo',
|
||||
deviceId: 123,
|
||||
persistentLogging: true,
|
||||
}),
|
||||
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
|
||||
}).enable();
|
||||
|
||||
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
|
||||
expect(await configJsonConfigBackend.get('deviceId')).to.equal(123);
|
||||
expect(await configJsonConfigBackend.get('persistentLogging')).to.equal(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should get object values for config.json "os" key', async () => {
|
||||
tfs = await testfs({
|
||||
[CONFIG_PATH]: JSON.stringify({
|
||||
os,
|
||||
}),
|
||||
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
|
||||
}).enable();
|
||||
|
||||
expect(await configJsonConfigBackend.get('os')).to.deep.equal(os);
|
||||
});
|
||||
|
||||
it('should get object values for config.json "os" key while "os" is empty', async () => {
|
||||
tfs = await testfs({
|
||||
[CONFIG_PATH]: JSON.stringify({}),
|
||||
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
|
||||
}).enable();
|
||||
|
||||
expect(await configJsonConfigBackend.get('os')).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should set primitive values for config.json key', async () => {
|
||||
tfs = await testfs({
|
||||
[CONFIG_PATH]: JSON.stringify({
|
||||
apiEndpoint: 'foo',
|
||||
deviceId: 123,
|
||||
persistentLogging: true,
|
||||
}),
|
||||
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
|
||||
}).enable();
|
||||
|
||||
await configJsonConfigBackend.set({
|
||||
apiEndpoint: 'bar',
|
||||
deviceId: 456,
|
||||
persistentLogging: false,
|
||||
});
|
||||
|
||||
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('bar');
|
||||
expect(await configJsonConfigBackend.get('deviceId')).to.equal(456);
|
||||
expect(await configJsonConfigBackend.get('persistentLogging')).to.equal(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set object values for config.json "os" key', async () => {
|
||||
tfs = await testfs({
|
||||
[CONFIG_PATH]: JSON.stringify({
|
||||
os,
|
||||
}),
|
||||
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
|
||||
}).enable();
|
||||
|
||||
const newOs = {
|
||||
power: {
|
||||
mode: 'low',
|
||||
},
|
||||
network: {
|
||||
wifi: {
|
||||
randomMacAddressScan: true,
|
||||
},
|
||||
},
|
||||
udevRules: {
|
||||
'56': 'ENV{ID_FS_LABEL_ENC}=="resin-root*", IMPORT{program}="resin_update_state_probe $devnode", SYMLINK+="disk/by-state/$env{RESIN_UPDATE_STATE}"',
|
||||
},
|
||||
sshKeys: ['ssh-rsa AAAAB3Nza...M2JB balena@macbook-pro'],
|
||||
};
|
||||
|
||||
await configJsonConfigBackend.set({
|
||||
os: newOs,
|
||||
});
|
||||
|
||||
expect(await configJsonConfigBackend.get('os')).to.deep.equal(newOs);
|
||||
});
|
||||
|
||||
it('should set object values for config.json "os" key while "os" is empty', async () => {
|
||||
tfs = await testfs({
|
||||
[CONFIG_PATH]: JSON.stringify({}),
|
||||
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
|
||||
}).enable();
|
||||
|
||||
await configJsonConfigBackend.set({
|
||||
os,
|
||||
});
|
||||
|
||||
expect(await configJsonConfigBackend.get('os')).to.deep.equal(os);
|
||||
});
|
||||
|
||||
// The following test cases may be unnecessary as they test cases where another party
|
||||
// writes to config.json directly (instead of through setting config vars on the API).
|
||||
it('should get cached value even if actual value has changed', async () => {
|
||||
tfs = await testfs({
|
||||
[CONFIG_PATH]: JSON.stringify({
|
||||
apiEndpoint: 'foo',
|
||||
}),
|
||||
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
|
||||
}).enable();
|
||||
|
||||
// The cached value should be returned
|
||||
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
|
||||
|
||||
// Change the value in the file
|
||||
await fs.writeFile(
|
||||
CONFIG_PATH,
|
||||
JSON.stringify({
|
||||
apiEndpoint: 'bar',
|
||||
}),
|
||||
);
|
||||
|
||||
// Unintended behavior: the cached value should not be overwritten
|
||||
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
|
||||
});
|
||||
|
||||
it('should set value and refresh cache to equal new value', async () => {
|
||||
tfs = await testfs({
|
||||
[CONFIG_PATH]: JSON.stringify({
|
||||
apiEndpoint: 'foo',
|
||||
}),
|
||||
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
|
||||
}).enable();
|
||||
|
||||
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
|
||||
|
||||
await fs.writeFile(
|
||||
CONFIG_PATH,
|
||||
JSON.stringify({
|
||||
apiEndpoint: 'bar',
|
||||
}),
|
||||
);
|
||||
|
||||
// Unintended behavior: cached value should not have been updated
|
||||
// as the change was not written to config.json by the Supervisor
|
||||
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
|
||||
|
||||
await configJsonConfigBackend.set({
|
||||
apiEndpoint: 'baz',
|
||||
});
|
||||
|
||||
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('baz');
|
||||
});
|
||||
});
|
577
test/integration/config/power-fan.spec.ts
Normal file
577
test/integration/config/power-fan.spec.ts
Normal file
@ -0,0 +1,577 @@
|
||||
import { expect } from 'chai';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import { testfs } from 'mocha-pod';
|
||||
import type { SinonStub } from 'sinon';
|
||||
|
||||
import { PowerFanConfig } from '~/src/config/backends/power-fan';
|
||||
import { Extlinux } from '~/src/config/backends/extlinux';
|
||||
import { ExtraUEnv } from '~/src/config/backends/extra-uEnv';
|
||||
import { ConfigTxt } from '~/src/config/backends/config-txt';
|
||||
import { ConfigFs } from '~/src/config/backends/config-fs';
|
||||
import { Odmdata } from '~/src/config/backends/odmdata';
|
||||
import { SplashImage } from '~/src/config/backends/splash-image';
|
||||
import ConfigJsonConfigBackend from '~/src/config/configJson';
|
||||
import { schema } from '~/src/config/schema';
|
||||
import * as hostUtils from '~/lib/host-utils';
|
||||
import log from '~/lib/supervisor-console';
|
||||
|
||||
const SUPPORTED_DEVICE_TYPES = [
|
||||
'jetson-agx-orin-devkit',
|
||||
'jetson-agx-orin-devkit-64gb',
|
||||
'jetson-orin-nano-devkit-nvme',
|
||||
'jetson-orin-nano-seeed-j3010',
|
||||
'jetson-orin-nx-seeed-j4012',
|
||||
'jetson-orin-nx-xavier-nx-devkit',
|
||||
];
|
||||
|
||||
const UNSUPPORTED_DEVICE_TYPES = ['jetson-orin-nx-xv3'];
|
||||
|
||||
describe('config/power-fan', () => {
|
||||
const CONFIG_PATH = hostUtils.pathOnBoot('config.json');
|
||||
const generateConfigJsonBackend = () => new ConfigJsonConfigBackend(schema);
|
||||
let powerFanConf: PowerFanConfig;
|
||||
|
||||
beforeEach(async () => {
|
||||
await testfs({
|
||||
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
|
||||
}).enable();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await testfs.restore();
|
||||
});
|
||||
|
||||
it('only matches supported devices', async () => {
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
for (const deviceType of SUPPORTED_DEVICE_TYPES) {
|
||||
expect(await powerFanConf.matches(deviceType)).to.be.true;
|
||||
}
|
||||
|
||||
for (const deviceType of UNSUPPORTED_DEVICE_TYPES) {
|
||||
expect(await powerFanConf.matches(deviceType)).to.be.false;
|
||||
}
|
||||
});
|
||||
|
||||
it('correctly gets boot configs from config.json', async () => {
|
||||
const getConfigJson = (powerMode: string, fanProfile: string) => {
|
||||
return stripIndent`
|
||||
{
|
||||
"os": {
|
||||
"extra": "field",
|
||||
"power": {
|
||||
"mode": "${powerMode}"
|
||||
},
|
||||
"fan": {
|
||||
"profile": "${fanProfile}"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
};
|
||||
|
||||
for (const powerMode of ['low', 'mid', 'high', 'custom_power']) {
|
||||
for (const fanProfile of ['quiet', 'default', 'cool', 'custom_fan']) {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: getConfigJson(powerMode, fanProfile),
|
||||
}).enable();
|
||||
|
||||
// ConfigJsonConfigBackend uses a cache, so setting a Supervisor-managed value
|
||||
// directly in config.json (thus circumventing ConfigJsonConfigBackend)
|
||||
// will not be reflected in the ConfigJsonConfigBackend instance.
|
||||
// We need to create a new instance which will recreate the cache
|
||||
// in order to get the latest value.
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
expect(await powerFanConf.getBootConfig()).to.deep.equal({
|
||||
power_mode: powerMode,
|
||||
fan_profile: fanProfile,
|
||||
});
|
||||
|
||||
await testfs.restore();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('correctly gets boot configs if power mode is not set', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: stripIndent`
|
||||
{
|
||||
"os": {
|
||||
"extra": "field",
|
||||
"fan": {
|
||||
"profile": "quiet"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
const config = await powerFanConf.getBootConfig();
|
||||
expect(config).to.deep.equal({
|
||||
fan_profile: 'quiet',
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly gets boot configs if fan profile is not set', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: stripIndent`
|
||||
{
|
||||
"os": {
|
||||
"extra": "field",
|
||||
"power": {
|
||||
"mode": "low"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
const config = await powerFanConf.getBootConfig();
|
||||
expect(config).to.deep.equal({
|
||||
power_mode: 'low',
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly gets boot configs if no relevant boot configs are set', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: stripIndent`
|
||||
{
|
||||
"os": {
|
||||
"extra": "field"
|
||||
}
|
||||
}`,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
const config = await powerFanConf.getBootConfig();
|
||||
expect(config).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('ignores unrelated fields in config.json when getting boot configs', async () => {
|
||||
const configStr = stripIndent`
|
||||
{
|
||||
"apiEndpoint": "https://api.balena-cloud.com",
|
||||
"uuid": "deadbeef",
|
||||
"os": {
|
||||
"power": {
|
||||
"mode": "low",
|
||||
"extra": "field"
|
||||
},
|
||||
"extra2": "field2",
|
||||
"fan": {
|
||||
"profile": "quiet",
|
||||
"extra3": "field3"
|
||||
},
|
||||
"network": {
|
||||
"connectivity": {
|
||||
"uri": "https://api.balena-cloud.com/connectivity-check",
|
||||
"interval": "300",
|
||||
"response": "optional value in the response"
|
||||
},
|
||||
"wifi": {
|
||||
"randomMacAddressScan": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
await testfs({
|
||||
[CONFIG_PATH]: configStr,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
const config = await powerFanConf.getBootConfig();
|
||||
expect(config).to.deep.equal({
|
||||
power_mode: 'low',
|
||||
fan_profile: 'quiet',
|
||||
});
|
||||
|
||||
// Check that unrelated fields are unchanged
|
||||
const configJson = await hostUtils.readFromBoot(CONFIG_PATH, 'utf-8');
|
||||
expect(configJson).to.equal(configStr);
|
||||
});
|
||||
|
||||
it('gets boot configs in config.json while current config is empty', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: '{}',
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
expect(await powerFanConf.getBootConfig()).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('sets boot configs in config.json', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: stripIndent`
|
||||
{
|
||||
"os": {
|
||||
"extra": "field"
|
||||
}
|
||||
}`,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
expect(await powerFanConf.getBootConfig()).to.deep.equal({});
|
||||
|
||||
await powerFanConf.setBootConfig({
|
||||
power_mode: 'low',
|
||||
fan_profile: 'quiet',
|
||||
});
|
||||
|
||||
expect(await powerFanConf.getBootConfig()).to.deep.equal({
|
||||
power_mode: 'low',
|
||||
fan_profile: 'quiet',
|
||||
});
|
||||
|
||||
// Sanity check that config.json is updated
|
||||
const configJson = await hostUtils.readFromBoot(CONFIG_PATH, 'utf-8');
|
||||
expect(configJson).to.deep.equal(
|
||||
JSON.stringify({
|
||||
os: {
|
||||
extra: 'field',
|
||||
power: {
|
||||
mode: 'low',
|
||||
},
|
||||
fan: {
|
||||
profile: 'quiet',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('sets boot configs in config.json while removing any unspecified boot configs', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: stripIndent`
|
||||
{
|
||||
"os": {
|
||||
"extra": "field",
|
||||
"power": {
|
||||
"mode": "low"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
await powerFanConf.setBootConfig({
|
||||
fan_profile: 'cool',
|
||||
});
|
||||
|
||||
const config = await powerFanConf.getBootConfig();
|
||||
expect(config).to.deep.equal({
|
||||
fan_profile: 'cool',
|
||||
});
|
||||
|
||||
// Sanity check that power mode is removed
|
||||
const configJson = await hostUtils.readFromBoot(CONFIG_PATH, 'utf-8');
|
||||
expect(configJson).to.deep.equal(
|
||||
JSON.stringify({
|
||||
os: {
|
||||
extra: 'field',
|
||||
fan: {
|
||||
profile: 'cool',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('sets boot configs in config.json while current config is empty', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: '{}',
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
await powerFanConf.setBootConfig({
|
||||
power_mode: 'low',
|
||||
fan_profile: 'quiet',
|
||||
});
|
||||
|
||||
expect(await powerFanConf.getBootConfig()).to.deep.equal({
|
||||
power_mode: 'low',
|
||||
fan_profile: 'quiet',
|
||||
});
|
||||
|
||||
// Sanity check that config.json is updated
|
||||
const configJson = await hostUtils.readFromBoot(CONFIG_PATH, 'utf-8');
|
||||
expect(configJson).to.deep.equal(
|
||||
JSON.stringify({
|
||||
os: {
|
||||
power: {
|
||||
mode: 'low',
|
||||
},
|
||||
fan: {
|
||||
profile: 'quiet',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('sets boot configs in config.json while current and target config are empty', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: '{}',
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
await powerFanConf.setBootConfig({});
|
||||
|
||||
expect(await powerFanConf.getBootConfig()).to.deep.equal({});
|
||||
|
||||
// Sanity check that config.json is empty
|
||||
const configJson = await hostUtils.readFromBoot(CONFIG_PATH, 'utf-8');
|
||||
expect(configJson).to.deep.equal(JSON.stringify({ os: {} }));
|
||||
});
|
||||
|
||||
it('handles setting configs correctly when target configs are empty string', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: stripIndent`
|
||||
{
|
||||
"os": {
|
||||
"extra": "field",
|
||||
"power": {
|
||||
"mode": "low"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
await powerFanConf.setBootConfig({
|
||||
fan_profile: '',
|
||||
});
|
||||
|
||||
const config = await powerFanConf.getBootConfig();
|
||||
expect(config).to.deep.equal({
|
||||
fan_profile: '',
|
||||
});
|
||||
|
||||
// Sanity check that config.json is updated
|
||||
const configJson = await hostUtils.readFromBoot(CONFIG_PATH, 'utf-8');
|
||||
expect(configJson).to.deep.equal(
|
||||
JSON.stringify({
|
||||
os: {
|
||||
extra: 'field',
|
||||
fan: {
|
||||
profile: '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not touch fields besides os.power and os.fan in config.json when setting boot configs', async () => {
|
||||
await testfs({
|
||||
// Note that extra fields in os.power and os.fan are removed when setting, as os.power
|
||||
// and os.fan are considered managed by the Supervisor.
|
||||
[CONFIG_PATH]: stripIndent`
|
||||
{
|
||||
"apiEndpoint": "https://api.balena-cloud.com",
|
||||
"uuid": "deadbeef",
|
||||
"os": {
|
||||
"power": {
|
||||
"mode": "low",
|
||||
"extra": "field"
|
||||
},
|
||||
"extra2": "field2",
|
||||
"fan": {
|
||||
"profile": "quiet",
|
||||
"extra3": "field3"
|
||||
},
|
||||
"network": {
|
||||
"connectivity": {
|
||||
"uri": "https://api.balena-cloud.com/connectivity-check",
|
||||
"interval": "300",
|
||||
"response": "optional value in the response"
|
||||
},
|
||||
"wifi": {
|
||||
"randomMacAddressScan": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
await powerFanConf.setBootConfig({
|
||||
power_mode: 'high',
|
||||
fan_profile: 'cool',
|
||||
});
|
||||
|
||||
expect(await powerFanConf.getBootConfig()).to.deep.equal({
|
||||
power_mode: 'high',
|
||||
fan_profile: 'cool',
|
||||
});
|
||||
|
||||
// Sanity check that os.power and os.fan are updated
|
||||
const configJson = await hostUtils.readFromBoot(CONFIG_PATH, 'utf-8');
|
||||
expect(configJson).to.deep.equal(
|
||||
JSON.stringify({
|
||||
apiEndpoint: 'https://api.balena-cloud.com',
|
||||
uuid: 'deadbeef',
|
||||
os: {
|
||||
power: {
|
||||
// Extra fields in os.power are removed when setting
|
||||
mode: 'high',
|
||||
},
|
||||
extra2: 'field2',
|
||||
fan: {
|
||||
// Extra fields in os.fan are removed when setting
|
||||
profile: 'cool',
|
||||
},
|
||||
network: {
|
||||
connectivity: {
|
||||
uri: 'https://api.balena-cloud.com/connectivity-check',
|
||||
interval: '300',
|
||||
response: 'optional value in the response',
|
||||
},
|
||||
wifi: {
|
||||
randomMacAddressScan: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not touch fields besides os.power and os.fan in config.json when removing boot configs', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: stripIndent`
|
||||
{
|
||||
"apiEndpoint": "https://api.balena-cloud.com",
|
||||
"uuid": "deadbeef",
|
||||
"os": {
|
||||
"power": {
|
||||
"mode": "low",
|
||||
"extra": "field"
|
||||
},
|
||||
"extra2": "field2",
|
||||
"fan": {
|
||||
"profile": "quiet",
|
||||
"extra3": "field3"
|
||||
},
|
||||
"network": {
|
||||
"connectivity": {
|
||||
"uri": "https://api.balena-cloud.com/connectivity-check",
|
||||
"interval": "300",
|
||||
"response": "optional value in the response"
|
||||
},
|
||||
"wifi": {
|
||||
"randomMacAddressScan": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
await powerFanConf.setBootConfig({});
|
||||
|
||||
expect(await powerFanConf.getBootConfig()).to.deep.equal({});
|
||||
|
||||
// Sanity check that os.power and os.fan are removed
|
||||
const configJson = await hostUtils.readFromBoot(CONFIG_PATH, 'utf-8');
|
||||
expect(configJson).to.deep.equal(
|
||||
JSON.stringify({
|
||||
apiEndpoint: 'https://api.balena-cloud.com',
|
||||
uuid: 'deadbeef',
|
||||
os: {
|
||||
extra2: 'field2',
|
||||
network: {
|
||||
connectivity: {
|
||||
uri: 'https://api.balena-cloud.com/connectivity-check',
|
||||
interval: '300',
|
||||
response: 'optional value in the response',
|
||||
},
|
||||
wifi: {
|
||||
randomMacAddressScan: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns empty object with warning if config.json cannot be parsed', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: 'not json',
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
(log.error as SinonStub).resetHistory();
|
||||
|
||||
const config = await powerFanConf.getBootConfig();
|
||||
expect(config).to.deep.equal({});
|
||||
expect(log.error as SinonStub).to.have.been.calledWithMatch(
|
||||
'Failed to read config.json while getting power / fan configs:',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns empty object if boot config does not have the right schema', async () => {
|
||||
await testfs({
|
||||
[CONFIG_PATH]: stripIndent`
|
||||
{
|
||||
"os": {
|
||||
"power": "not an object",
|
||||
"fan": "also not an object",
|
||||
"extra": "field"
|
||||
}
|
||||
}`,
|
||||
}).enable();
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
expect(await powerFanConf.getBootConfig()).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('is the only config backend that supports power mode and fan profile', () => {
|
||||
const otherBackends = [
|
||||
new Extlinux(),
|
||||
new ExtraUEnv(),
|
||||
new ConfigTxt(),
|
||||
new ConfigFs(),
|
||||
new Odmdata(),
|
||||
new SplashImage(),
|
||||
];
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
|
||||
for (const config of ['power_mode', 'fan_profile']) {
|
||||
for (const backend of otherBackends) {
|
||||
expect(backend.isBootConfigVar(`HOST_CONFIG_${config}`)).to.be.false;
|
||||
expect(backend.isSupportedConfig(config)).to.be.false;
|
||||
}
|
||||
|
||||
expect(powerFanConf.isBootConfigVar(`HOST_CONFIG_${config}`)).to.be.true;
|
||||
expect(powerFanConf.isSupportedConfig(config)).to.be.true;
|
||||
}
|
||||
});
|
||||
|
||||
it('converts supported config vars to boot configs regardless of case', () => {
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
for (const config of ['power_mode', 'fan_profile']) {
|
||||
expect(
|
||||
powerFanConf.processConfigVarName(`HOST_CONFIG_${config}`),
|
||||
).to.equal(config);
|
||||
expect(
|
||||
powerFanConf.processConfigVarName(
|
||||
`HOST_CONFIG_${config.toUpperCase()}`,
|
||||
),
|
||||
).to.equal(config);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows any value for power mode and fan profile', () => {
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
for (const config of ['power_mode', 'fan_profile']) {
|
||||
expect(powerFanConf.processConfigVarValue(config, 'any value')).to.equal(
|
||||
'any value',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('creates supported config vars from boot configs', () => {
|
||||
powerFanConf = new PowerFanConfig(generateConfigJsonBackend());
|
||||
for (const config of ['power_mode', 'fan_profile']) {
|
||||
expect(powerFanConf.createConfigVarName(config)).to.equal(
|
||||
`HOST_CONFIG_${config}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
@ -7,6 +7,8 @@ import { Extlinux } from '~/src/config/backends/extlinux';
|
||||
import { ConfigTxt } from '~/src/config/backends/config-txt';
|
||||
import { ConfigFs } from '~/src/config/backends/config-fs';
|
||||
import { SplashImage } from '~/src/config/backends/splash-image';
|
||||
import { PowerFanConfig } from '~/src/config/backends/power-fan';
|
||||
import { configJsonBackend } from '~/src/config';
|
||||
import type { ConfigBackend } from '~/src/config/backends/backend';
|
||||
|
||||
import * as hostUtils from '~/lib/host-utils';
|
||||
@ -63,6 +65,7 @@ const BACKENDS: Record<string, ConfigBackend> = {
|
||||
configtxt: new ConfigTxt(),
|
||||
configfs: new ConfigFs(),
|
||||
splashImage: new SplashImage(),
|
||||
powerFan: new PowerFanConfig(configJsonBackend),
|
||||
};
|
||||
|
||||
const CONFIGS = {
|
||||
@ -123,4 +126,14 @@ const CONFIGS = {
|
||||
// ssdt: ['spidev1,1']
|
||||
// },
|
||||
// },
|
||||
powerFan: {
|
||||
envVars: {
|
||||
HOST_CONFIG_power_mode: 'low',
|
||||
HOST_CONFIG_fan_profile: 'quiet',
|
||||
},
|
||||
bootConfig: {
|
||||
power_mode: 'low',
|
||||
fan_profile: 'quiet',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user