mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-20 17:52:51 +00:00
refactor: Convert DeviceConfig module to typescript
Change-type: patch Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
parent
7ba1ab981c
commit
b32fba43e1
@ -329,13 +329,13 @@ module.exports = class APIBinder
|
||||
# Creates the necessary config vars in the API to match the current device state,
|
||||
# without overwriting any variables that are already set.
|
||||
_reportInitialEnv: (apiEndpoint) =>
|
||||
defaultConfig = @deviceState.deviceConfig.getDefaults()
|
||||
Promise.join(
|
||||
@deviceState.getCurrentForComparison()
|
||||
@getTargetState().then (targetState) =>
|
||||
@deviceState.deviceConfig.formatConfigKeys(targetState.local.config)
|
||||
@deviceState.deviceConfig.getDefaults()
|
||||
@config.get('deviceId')
|
||||
(currentState, targetConfig, defaultConfig, deviceId) =>
|
||||
(currentState, targetConfig, deviceId) =>
|
||||
currentConfig = currentState.local.config
|
||||
Promise.mapSeries _.toPairs(currentConfig), ([ key, value ]) =>
|
||||
# We want to disable local mode when joining a cloud
|
||||
|
@ -1,235 +0,0 @@
|
||||
Promise = require 'bluebird'
|
||||
_ = require 'lodash'
|
||||
|
||||
systemd = require './lib/systemd'
|
||||
{ checkTruthy, checkInt } = require './lib/validation'
|
||||
{ UnitNotLoadedError } = require './lib/errors'
|
||||
configUtils = require './config/utils'
|
||||
|
||||
vpnServiceName = 'openvpn-resin'
|
||||
|
||||
module.exports = class DeviceConfig
|
||||
constructor: ({ @db, @config, @logger }) ->
|
||||
@rebootRequired = false
|
||||
@configKeys = {
|
||||
appUpdatePollInterval: { envVarName: 'SUPERVISOR_POLL_INTERVAL', varType: 'int', defaultValue: '60000' }
|
||||
localMode: { envVarName: 'SUPERVISOR_LOCAL_MODE', varType: 'bool', defaultValue: 'false' }
|
||||
connectivityCheckEnabled: { envVarName: 'SUPERVISOR_CONNECTIVITY_CHECK', varType: 'bool', defaultValue: 'true' }
|
||||
loggingEnabled: { envVarName: 'SUPERVISOR_LOG_CONTROL', varType: 'bool', defaultValue: 'true' }
|
||||
delta: { envVarName: 'SUPERVISOR_DELTA', varType: 'bool', defaultValue: 'false' }
|
||||
deltaRequestTimeout: { envVarName: 'SUPERVISOR_DELTA_REQUEST_TIMEOUT', varType: 'int', defaultValue: '30000' }
|
||||
deltaApplyTimeout: { envVarName: 'SUPERVISOR_DELTA_APPLY_TIMEOUT', varType: 'int', defaultValue: '' }
|
||||
deltaRetryCount: { envVarName: 'SUPERVISOR_DELTA_RETRY_COUNT', varType: 'int', defaultValue: '30' }
|
||||
deltaRetryInterval: { envVarName: 'SUPERVISOR_DELTA_RETRY_INTERVAL', varType: 'int', defaultValue: '10000' }
|
||||
deltaVersion: { envVarName: 'SUPERVISOR_DELTA_VERSION', varType: 'int', defaultValue: '2' }
|
||||
lockOverride: { envVarName: 'SUPERVISOR_OVERRIDE_LOCK', varType: 'bool', defaultValue: 'false' }
|
||||
persistentLogging: { envVarName: 'SUPERVISOR_PERSISTENT_LOGGING', varType: 'bool', defaultValue: 'false', rebootRequired: true }
|
||||
}
|
||||
@validKeys = [
|
||||
'SUPERVISOR_VPN_CONTROL',
|
||||
'OVERRIDE_LOCK',
|
||||
].concat(_.map(@configKeys, 'envVarName'))
|
||||
@actionExecutors = {
|
||||
changeConfig: (step) =>
|
||||
@logger.logConfigChange(step.humanReadableTarget)
|
||||
@config.set(step.target)
|
||||
.then =>
|
||||
@logger.logConfigChange(step.humanReadableTarget, { success: true })
|
||||
if step.rebootRequired
|
||||
@rebootRequired = true
|
||||
.tapCatch (err) =>
|
||||
@logger.logConfigChange(step.humanReadableTarget, { err })
|
||||
setVPNEnabled: (step, { initial = false } = {}) =>
|
||||
logValue = { SUPERVISOR_VPN_CONTROL: step.target }
|
||||
if !initial
|
||||
@logger.logConfigChange(logValue)
|
||||
@setVPNEnabled(step.target)
|
||||
.then =>
|
||||
if !initial
|
||||
@logger.logConfigChange(logValue, { success: true })
|
||||
.tapCatch (err) =>
|
||||
@logger.logConfigChange(logValue, { err })
|
||||
setBootConfig: (step) =>
|
||||
@getConfigBackend()
|
||||
.then (configBackend ) =>
|
||||
@setBootConfig(configBackend, step.target)
|
||||
}
|
||||
@validActions = _.keys(@actionExecutors)
|
||||
@configBackend = null
|
||||
|
||||
getConfigBackend: =>
|
||||
if @configBackend?
|
||||
Promise.resolve(@configBackend)
|
||||
else
|
||||
@config.get('deviceType').then (deviceType) =>
|
||||
@configBackend = configUtils.getConfigBackend(deviceType)
|
||||
return @configBackend
|
||||
|
||||
setTarget: (target, trx) =>
|
||||
db = trx ? @db.models.bind(@db)
|
||||
@formatConfigKeys(target)
|
||||
.then (formattedTarget) ->
|
||||
confToUpdate = {
|
||||
targetValues: JSON.stringify(formattedTarget)
|
||||
}
|
||||
db('deviceConfig').update(confToUpdate)
|
||||
|
||||
getTarget: ({ initial = false } = {}) =>
|
||||
Promise.all([
|
||||
@config.get('unmanaged')
|
||||
@db.models('deviceConfig').select('targetValues')
|
||||
])
|
||||
.then ([unmanaged, [ devConfig ]]) =>
|
||||
conf = JSON.parse(devConfig.targetValues)
|
||||
if initial or !conf.SUPERVISOR_VPN_CONTROL?
|
||||
conf.SUPERVISOR_VPN_CONTROL = 'true'
|
||||
if unmanaged and !conf.SUPERVISOR_LOCAL_MODE?
|
||||
conf.SUPERVISOR_LOCAL_MODE = 'true'
|
||||
for own k, { envVarName, defaultValue } of @configKeys
|
||||
conf[envVarName] ?= defaultValue
|
||||
return conf
|
||||
|
||||
getCurrent: =>
|
||||
Promise.all [
|
||||
@config.getMany([ 'deviceType' ].concat(_.keys(@configKeys)))
|
||||
@getConfigBackend()
|
||||
]
|
||||
.then ([ conf, configBackend ]) =>
|
||||
Promise.join(
|
||||
@getVPNEnabled()
|
||||
@getBootConfig(configBackend)
|
||||
(vpnStatus, bootConfig) =>
|
||||
currentConf = {
|
||||
SUPERVISOR_VPN_CONTROL: (vpnStatus ? 'true').toString()
|
||||
}
|
||||
for own key, { envVarName } of @configKeys
|
||||
currentConf[envVarName] = (conf[key] ? '').toString()
|
||||
return _.assign(currentConf, bootConfig)
|
||||
)
|
||||
|
||||
formatConfigKeys: (conf) =>
|
||||
@getConfigBackend()
|
||||
.then (configBackend) =>
|
||||
configUtils.formatConfigKeys(configBackend, @validKeys, conf)
|
||||
|
||||
getDefaults: =>
|
||||
Promise.try =>
|
||||
return _.extend({
|
||||
SUPERVISOR_VPN_CONTROL: 'true'
|
||||
}, _.mapValues(_.mapKeys(@configKeys, 'envVarName'), 'defaultValue'))
|
||||
|
||||
bootConfigChangeRequired: (configBackend, current, target) =>
|
||||
targetBootConfig = configUtils.envToBootConfig(configBackend, target)
|
||||
currentBootConfig = configUtils.envToBootConfig(configBackend, current)
|
||||
|
||||
if !_.isEqual(currentBootConfig, targetBootConfig)
|
||||
_.each targetBootConfig, (value, key) =>
|
||||
if not configBackend.isSupportedConfig(key)
|
||||
if currentBootConfig[key] != value
|
||||
err = "Attempt to change blacklisted config value #{key}"
|
||||
@logger.logSystemMessage(err, { error: err }, 'Apply boot config error')
|
||||
throw new Error(err)
|
||||
return true
|
||||
return false
|
||||
|
||||
getRequiredSteps: (currentState, targetState) =>
|
||||
current = _.clone(currentState.local?.config ? {})
|
||||
target = _.clone(targetState.local?.config ? {})
|
||||
steps = []
|
||||
Promise.all [
|
||||
@config.getMany([ 'deviceType', 'unmanaged' ])
|
||||
@getConfigBackend()
|
||||
]
|
||||
.then ([{ deviceType, unmanaged }, configBackend ]) =>
|
||||
configChanges = {}
|
||||
humanReadableConfigChanges = {}
|
||||
match = {
|
||||
'bool': (a, b) ->
|
||||
checkTruthy(a) == checkTruthy(b)
|
||||
'int': (a, b) ->
|
||||
checkInt(a) == checkInt(b)
|
||||
}
|
||||
# If the legacy lock override is used, place it as the new variable
|
||||
if checkTruthy(target['OVERRIDE_LOCK'])
|
||||
target['SUPERVISOR_OVERRIDE_LOCK'] = target['OVERRIDE_LOCK']
|
||||
reboot = false
|
||||
for own key, { envVarName, varType, rebootRequired } of @configKeys
|
||||
if !match[varType](current[envVarName], target[envVarName])
|
||||
configChanges[key] = target[envVarName]
|
||||
humanReadableConfigChanges[envVarName] = target[envVarName]
|
||||
reboot = reboot || (rebootRequired ? false)
|
||||
if !_.isEmpty(configChanges)
|
||||
steps.push({
|
||||
action: 'changeConfig'
|
||||
target: configChanges
|
||||
humanReadableTarget: humanReadableConfigChanges
|
||||
rebootRequired: reboot
|
||||
})
|
||||
return
|
||||
|
||||
# Check if we need to perform special case actions for the VPN
|
||||
if !checkTruthy(unmanaged) &&
|
||||
!_.isEmpty(target['SUPERVISOR_VPN_CONTROL']) &&
|
||||
@checkBoolChanged(current, target, 'SUPERVISOR_VPN_CONTROL')
|
||||
steps.push({
|
||||
action: 'setVPNEnabled'
|
||||
target: target['SUPERVISOR_VPN_CONTROL']
|
||||
})
|
||||
|
||||
# Do we need to change the boot config?
|
||||
if @bootConfigChangeRequired(configBackend, current, target)
|
||||
steps.push({
|
||||
action: 'setBootConfig'
|
||||
target
|
||||
})
|
||||
|
||||
if !_.isEmpty(steps)
|
||||
return
|
||||
if @rebootRequired
|
||||
steps.push({
|
||||
action: 'reboot'
|
||||
})
|
||||
return
|
||||
.return(steps)
|
||||
|
||||
executeStepAction: (step, opts) =>
|
||||
@actionExecutors[step.action](step, opts)
|
||||
|
||||
getBootConfig: (configBackend) ->
|
||||
Promise.try ->
|
||||
if !configBackend?
|
||||
return {}
|
||||
configBackend.getBootConfig()
|
||||
.then (config) ->
|
||||
return configUtils.bootConfigToEnv(configBackend, config)
|
||||
|
||||
setBootConfig: (configBackend, target) =>
|
||||
Promise.try =>
|
||||
if !configBackend?
|
||||
return false
|
||||
conf = configUtils.envToBootConfig(configBackend, target)
|
||||
@logger.logSystemMessage("Applying boot config: #{JSON.stringify(conf)}", {}, 'Apply boot config in progress')
|
||||
|
||||
configBackend.setBootConfig(conf)
|
||||
.then =>
|
||||
@logger.logSystemMessage("Applied boot config: #{JSON.stringify(conf)}", {}, 'Apply boot config success')
|
||||
@rebootRequired = true
|
||||
return true
|
||||
.tapCatch (err) =>
|
||||
@logger.logSystemMessage("Error setting boot config: #{err}", { error: err }, 'Apply boot config error')
|
||||
|
||||
getVPNEnabled: ->
|
||||
systemd.serviceActiveState(vpnServiceName)
|
||||
.then (activeState) ->
|
||||
return activeState not in [ 'inactive', 'deactivating' ]
|
||||
.catchReturn(UnitNotLoadedError, null)
|
||||
|
||||
setVPNEnabled: (val) ->
|
||||
enable = checkTruthy(val) ? true
|
||||
if enable
|
||||
systemd.startService(vpnServiceName)
|
||||
else
|
||||
systemd.stopService(vpnServiceName)
|
||||
|
||||
checkBoolChanged: (current, target, key) ->
|
||||
checkTruthy(current[key]) != checkTruthy(target[key])
|
518
src/device-config.ts
Normal file
518
src/device-config.ts
Normal file
@ -0,0 +1,518 @@
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import Config = require('./config');
|
||||
import Database, { Transaction } from './db';
|
||||
import Logger from './logger';
|
||||
|
||||
import { DeviceConfigBackend } from './config/backend';
|
||||
import * as configUtils from './config/utils';
|
||||
import { UnitNotLoadedError } from './lib/errors';
|
||||
import * as systemd from './lib/systemd';
|
||||
import { EnvVarObject } from './lib/types';
|
||||
import { checkInt, checkTruthy } from './lib/validation';
|
||||
import { DeviceApplicationState } from './types/state';
|
||||
|
||||
const vpnServiceName = 'openvpn-resin';
|
||||
|
||||
interface DeviceConfigConstructOpts {
|
||||
db: Database;
|
||||
config: Config;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
interface ConfigOption {
|
||||
envVarName: string;
|
||||
varType: string;
|
||||
defaultValue: string;
|
||||
rebootRequired?: boolean;
|
||||
}
|
||||
|
||||
interface ConfigStep {
|
||||
// TODO: This is a bit of a mess, the DeviceConfig class shouldn't
|
||||
// know that the reboot action exists as it is implemented by
|
||||
// DeviceState. Fix this weird circular dependency
|
||||
action: keyof DeviceActionExecutors | 'reboot';
|
||||
humanReadableTarget?: Dictionary<string>;
|
||||
target?: string | Dictionary<string>;
|
||||
rebootRequired?: boolean;
|
||||
}
|
||||
|
||||
interface DeviceActionExecutorOpts {
|
||||
initial?: boolean;
|
||||
}
|
||||
|
||||
type DeviceActionExecutorFn = (
|
||||
step: ConfigStep,
|
||||
opts?: DeviceActionExecutorOpts,
|
||||
) => Promise<void>;
|
||||
|
||||
interface DeviceActionExecutors {
|
||||
changeConfig: DeviceActionExecutorFn;
|
||||
setVPNEnabled: DeviceActionExecutorFn;
|
||||
setBootConfig: DeviceActionExecutorFn;
|
||||
}
|
||||
|
||||
export class DeviceConfig {
|
||||
private db: Database;
|
||||
private config: Config;
|
||||
private logger: Logger;
|
||||
private rebootRequired = false;
|
||||
private actionExecutors: DeviceActionExecutors;
|
||||
private configBackend: DeviceConfigBackend | null = null;
|
||||
|
||||
private static configKeys: Dictionary<ConfigOption> = {
|
||||
appUpdatePollInterval: {
|
||||
envVarName: 'SUPERVISOR_POLL_INTERVAL',
|
||||
varType: 'int',
|
||||
defaultValue: '60000',
|
||||
},
|
||||
localMode: {
|
||||
envVarName: 'SUPERVISOR_LOCAL_MODE',
|
||||
varType: 'bool',
|
||||
defaultValue: 'false',
|
||||
},
|
||||
connectivityCheckEnabled: {
|
||||
envVarName: 'SUPERVISOR_CONNECTIVITY_CHECK',
|
||||
varType: 'bool',
|
||||
defaultValue: 'true',
|
||||
},
|
||||
loggingEnabled: {
|
||||
envVarName: 'SUPERVISOR_LOG_CONTROL',
|
||||
varType: 'bool',
|
||||
defaultValue: 'true',
|
||||
},
|
||||
delta: {
|
||||
envVarName: 'SUPERVISOR_DELTA',
|
||||
varType: 'bool',
|
||||
defaultValue: 'false',
|
||||
},
|
||||
deltaRequestTimeout: {
|
||||
envVarName: 'SUPERVISOR_DELTA_REQUEST_TIMEOUT',
|
||||
varType: 'int',
|
||||
defaultValue: '30000',
|
||||
},
|
||||
deltaApplyTimeout: {
|
||||
envVarName: 'SUPERVISOR_DELTA_APPLY_TIMEOUT',
|
||||
varType: 'int',
|
||||
defaultValue: '',
|
||||
},
|
||||
deltaRetryCount: {
|
||||
envVarName: 'SUPERVISOR_DELTA_RETRY_COUNT',
|
||||
varType: 'int',
|
||||
defaultValue: '30',
|
||||
},
|
||||
deltaRetryInterval: {
|
||||
envVarName: 'SUPERVISOR_DELTA_RETRY_INTERVAL',
|
||||
varType: 'int',
|
||||
defaultValue: '10000',
|
||||
},
|
||||
deltaVersion: {
|
||||
envVarName: 'SUPERVISOR_DELTA_VERSION',
|
||||
varType: 'int',
|
||||
defaultValue: '2',
|
||||
},
|
||||
lockOverride: {
|
||||
envVarName: 'SUPERVISOR_OVERRIDE_LOCK',
|
||||
varType: 'bool',
|
||||
defaultValue: 'false',
|
||||
},
|
||||
persistentLogging: {
|
||||
envVarName: 'SUPERVISOR_PERSISTENT_LOGGING',
|
||||
varType: 'bool',
|
||||
defaultValue: 'false',
|
||||
rebootRequired: true,
|
||||
},
|
||||
};
|
||||
|
||||
static validKeys = [
|
||||
'SUPERVISOR_VPN_CONTROL',
|
||||
'OVERRRIDE_LOCK',
|
||||
..._.map(DeviceConfig.configKeys, 'envVarName'),
|
||||
];
|
||||
|
||||
public constructor({ db, config, logger }: DeviceConfigConstructOpts) {
|
||||
this.db = db;
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
|
||||
this.actionExecutors = {
|
||||
changeConfig: async step => {
|
||||
try {
|
||||
if (step.humanReadableTarget) {
|
||||
this.logger.logConfigChange(step.humanReadableTarget);
|
||||
}
|
||||
if (!_.isObject(step.target)) {
|
||||
throw new Error('Non-dictionary value passed to changeConfig');
|
||||
}
|
||||
await this.config.set(step.target as Dictionary<string>);
|
||||
if (step.humanReadableTarget) {
|
||||
this.logger.logConfigChange(step.humanReadableTarget, {
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
if (step.rebootRequired) {
|
||||
this.rebootRequired = true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (step.humanReadableTarget) {
|
||||
this.logger.logConfigChange(step.humanReadableTarget, {
|
||||
err,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
setVPNEnabled: async (step, opts = {}) => {
|
||||
const { initial = false } = opts;
|
||||
if (!_.isString(step.target)) {
|
||||
throw new Error('Non-string value passed to setVPNEnabled');
|
||||
}
|
||||
const logValue = { SUPERVISOR_VPN_CONTROL: step.target };
|
||||
if (!initial) {
|
||||
this.logger.logConfigChange(logValue);
|
||||
}
|
||||
try {
|
||||
await this.setVPNEnabled(step.target);
|
||||
if (!initial) {
|
||||
this.logger.logConfigChange(logValue, { success: true });
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.logConfigChange(logValue, { err });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
setBootConfig: async step => {
|
||||
const configBackend = await this.getConfigBackend();
|
||||
if (!_.isObject(step.target)) {
|
||||
throw new Error(
|
||||
'Non-dictionary passed to DeviceConfig.setBootConfig',
|
||||
);
|
||||
}
|
||||
await this.setBootConfig(configBackend, step.target as Dictionary<
|
||||
string
|
||||
>);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getConfigBackend() {
|
||||
if (this.configBackend != null) {
|
||||
return this.configBackend;
|
||||
}
|
||||
const dt = await this.config.get('deviceType');
|
||||
if (!_.isString(dt)) {
|
||||
throw new Error('Could not detect device type');
|
||||
}
|
||||
|
||||
this.configBackend = configUtils.getConfigBackend(dt) || null;
|
||||
|
||||
return this.configBackend;
|
||||
}
|
||||
|
||||
public async setTarget(
|
||||
target: Dictionary<string>,
|
||||
trx?: Transaction,
|
||||
): Promise<void> {
|
||||
const db = trx != null ? trx : this.db.models.bind(this.db);
|
||||
|
||||
const formatted = await this.formatConfigKeys(target);
|
||||
const confToUpdate = {
|
||||
targetValues: JSON.stringify(formatted),
|
||||
};
|
||||
await db('deviceConfig').update(confToUpdate);
|
||||
}
|
||||
|
||||
public async getTarget({ initial = false }: { initial?: boolean } = {}) {
|
||||
const [unmanaged, [devConfig]] = await Promise.all([
|
||||
this.config.get('unmanaged'),
|
||||
this.db.models('deviceConfig').select('targetValues'),
|
||||
]);
|
||||
|
||||
let conf: Dictionary<string>;
|
||||
try {
|
||||
conf = JSON.parse(devConfig.targetValues);
|
||||
} catch (e) {
|
||||
throw new Error(`Corrupted supervisor database! Error: ${e.message}`);
|
||||
}
|
||||
if (initial || conf.SUPERVISOR_VPN_CONTROL == null) {
|
||||
conf.SUPERVISOR_VPN_CONTROL = 'true';
|
||||
}
|
||||
if (unmanaged && conf.SUPERVISOR_LOCAL_MODE == null) {
|
||||
conf.SUPERVISOR_LOCAL_MODE = 'true';
|
||||
}
|
||||
|
||||
_.defaults(
|
||||
conf,
|
||||
_(DeviceConfig.configKeys)
|
||||
.mapKeys('envVarName')
|
||||
.mapValues('defaultValue')
|
||||
.value(),
|
||||
);
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
public async getCurrent() {
|
||||
const conf = await this.config.getMany(
|
||||
['deviceType'].concat(_.keys(DeviceConfig.configKeys)),
|
||||
);
|
||||
|
||||
const configBackend = await this.getConfigBackend();
|
||||
|
||||
const [vpnStatus, bootConfig] = await Promise.all([
|
||||
this.getVPNEnabled(),
|
||||
this.getBootConfig(configBackend),
|
||||
]);
|
||||
|
||||
const currentConf: Dictionary<string> = {
|
||||
// TODO: Fix this mess of half strings half boolean values everywhere
|
||||
SUPERVISOR_VPN_CONTROL: vpnStatus != null ? vpnStatus.toString() : 'true',
|
||||
};
|
||||
|
||||
for (const key in DeviceConfig.configKeys) {
|
||||
const { envVarName } = DeviceConfig.configKeys[key];
|
||||
const confValue = conf[key];
|
||||
currentConf[envVarName] = confValue != null ? confValue.toString() : '';
|
||||
}
|
||||
|
||||
return _.assign(currentConf, bootConfig);
|
||||
}
|
||||
|
||||
public async formatConfigKeys(
|
||||
conf: Dictionary<string>,
|
||||
): Promise<Dictionary<any>> {
|
||||
const backend = await this.getConfigBackend();
|
||||
return await configUtils.formatConfigKeys(
|
||||
backend,
|
||||
DeviceConfig.validKeys,
|
||||
conf,
|
||||
);
|
||||
}
|
||||
|
||||
public getDefaults() {
|
||||
return _.extend(
|
||||
{
|
||||
SUPERVISOR_VPN_CONTROL: 'true',
|
||||
},
|
||||
_.mapValues(
|
||||
_.mapKeys(DeviceConfig.configKeys, 'envVarName'),
|
||||
'defaultValues',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private bootConfigChangeRequired(
|
||||
configBackend: DeviceConfigBackend | null,
|
||||
current: Dictionary<string>,
|
||||
target: Dictionary<string>,
|
||||
): boolean {
|
||||
const targetBootConfig = configUtils.envToBootConfig(configBackend, target);
|
||||
const currentBootConfig = configUtils.envToBootConfig(
|
||||
configBackend,
|
||||
current,
|
||||
);
|
||||
|
||||
if (!_.isEqual(currentBootConfig, targetBootConfig)) {
|
||||
_.each(targetBootConfig, (value, key) => {
|
||||
// Ignore null check because we can't get here if configBackend is null
|
||||
if (!configBackend!.isSupportedConfig(key)) {
|
||||
if (currentBootConfig[key] !== value) {
|
||||
const err = `Attempt to change blacklisted config value ${key}`;
|
||||
this.logger.logSystemMessage(
|
||||
err,
|
||||
{ error: err },
|
||||
'Apply boot config error',
|
||||
);
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async getRequiredSteps(
|
||||
currentState: DeviceApplicationState,
|
||||
targetState: DeviceApplicationState,
|
||||
): Promise<ConfigStep[]> {
|
||||
const current: Dictionary<string> = _.get(
|
||||
currentState,
|
||||
['local', 'config'],
|
||||
{},
|
||||
);
|
||||
const target: Dictionary<string> = _.get(
|
||||
targetState,
|
||||
['local', 'config'],
|
||||
{},
|
||||
);
|
||||
|
||||
const steps: ConfigStep[] = [];
|
||||
|
||||
const unmanaged = await this.config.get('unmanaged');
|
||||
const backend = await this.getConfigBackend();
|
||||
|
||||
const configChanges: Dictionary<string> = {};
|
||||
const humanReadableConfigChanges: Dictionary<string> = {};
|
||||
let reboot = false;
|
||||
|
||||
// If the legacy lock override is used, place it as the new variable
|
||||
if (checkTruthy(target['OVERRIDE_LOCK'])) {
|
||||
target['SUPERVISOR_OVERRIDE_LOCK'] = target['OVERRIDE_LOCK'];
|
||||
}
|
||||
|
||||
_.each(
|
||||
DeviceConfig.configKeys,
|
||||
({ envVarName, varType, rebootRequired }, key) => {
|
||||
// Test if the key is different
|
||||
if (
|
||||
!DeviceConfig.configTest(
|
||||
varType,
|
||||
current[envVarName],
|
||||
target[envVarName],
|
||||
)
|
||||
) {
|
||||
// Save the change if it is
|
||||
configChanges[key] = target[envVarName];
|
||||
humanReadableConfigChanges[envVarName] = target[envVarName];
|
||||
reboot = rebootRequired || reboot;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!_.isEmpty(configChanges)) {
|
||||
steps.push({
|
||||
action: 'changeConfig',
|
||||
target: configChanges,
|
||||
humanReadableTarget: humanReadableConfigChanges,
|
||||
rebootRequired: reboot,
|
||||
});
|
||||
}
|
||||
|
||||
// Check for special case actions for the VPN
|
||||
if (
|
||||
!checkTruthy(unmanaged || false) &&
|
||||
!_.isEmpty(target['SUPERVISOR_VPN_CONTROL']) &&
|
||||
DeviceConfig.checkBoolChanged(current, target, 'SUPERVISOR_VPN_CONTROL')
|
||||
) {
|
||||
steps.push({
|
||||
action: 'setVPNEnabled',
|
||||
target: target['SUPERVISOR_VPN_CONTROL'],
|
||||
});
|
||||
}
|
||||
|
||||
// Do we need to change the boot config?
|
||||
if (this.bootConfigChangeRequired(backend, current, target)) {
|
||||
steps.push({
|
||||
action: 'setBootConfig',
|
||||
target,
|
||||
});
|
||||
}
|
||||
|
||||
if (_.isEmpty(steps) && this.rebootRequired) {
|
||||
steps.push({
|
||||
action: 'reboot',
|
||||
});
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
public executeStepAction(step: ConfigStep, opts: DeviceActionExecutorOpts) {
|
||||
if (step.action !== 'reboot') {
|
||||
return this.actionExecutors[step.action](step, opts);
|
||||
}
|
||||
}
|
||||
|
||||
public isValidAction(action: string): boolean {
|
||||
return _.includes(_.keys(this.actionExecutors), action);
|
||||
}
|
||||
|
||||
private async getBootConfig(
|
||||
backend: DeviceConfigBackend | null,
|
||||
): Promise<EnvVarObject> {
|
||||
if (backend == null) {
|
||||
return {};
|
||||
}
|
||||
const conf = await backend.getBootConfig();
|
||||
return configUtils.bootConfigToEnv(backend, conf);
|
||||
}
|
||||
|
||||
private async setBootConfig(
|
||||
backend: DeviceConfigBackend | null,
|
||||
target: Dictionary<string>,
|
||||
) {
|
||||
if (backend == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const conf = configUtils.envToBootConfig(backend, target);
|
||||
this.logger.logSystemMessage(
|
||||
`Applying boot config: ${JSON.stringify(conf)}`,
|
||||
{},
|
||||
'Apply boot config in progress',
|
||||
);
|
||||
|
||||
try {
|
||||
await backend.setBootConfig(conf);
|
||||
this.logger.logSystemMessage(
|
||||
`Applied boot config: ${JSON.stringify(conf)}`,
|
||||
{},
|
||||
'Apply boot config success',
|
||||
);
|
||||
this.rebootRequired = true;
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.logger.logSystemMessage(
|
||||
`Error setting boot config: ${err}`,
|
||||
{ error: err },
|
||||
'Apply boot config error',
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async getVPNEnabled(): Promise<boolean> {
|
||||
try {
|
||||
const activeState = await systemd.serviceActiveState(vpnServiceName);
|
||||
return !_.includes(['inactive', 'deactivating'], activeState);
|
||||
} catch (e) {
|
||||
if (UnitNotLoadedError(e)) {
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async setVPNEnabled(value?: string | boolean) {
|
||||
const v = checkTruthy(value || true);
|
||||
const enable = v != null ? v : true;
|
||||
|
||||
if (enable) {
|
||||
await systemd.startService(vpnServiceName);
|
||||
} else {
|
||||
await systemd.stopService(vpnServiceName);
|
||||
}
|
||||
}
|
||||
|
||||
private static configTest(method: string, a: string, b: string): boolean {
|
||||
switch (method) {
|
||||
case 'bool':
|
||||
return checkTruthy(a) === checkTruthy(b);
|
||||
case 'int':
|
||||
return checkInt(a) === checkInt(b);
|
||||
default:
|
||||
throw new Error('Incorrect datatype passed to DeviceConfig.configTest');
|
||||
}
|
||||
}
|
||||
|
||||
private static checkBoolChanged(
|
||||
current: Dictionary<string>,
|
||||
target: Dictionary<string>,
|
||||
key: string,
|
||||
): boolean {
|
||||
return checkTruthy(current[key]) !== checkTruthy(target[key]);
|
||||
}
|
||||
}
|
||||
|
||||
export default DeviceConfig;
|
@ -19,7 +19,7 @@ updateLock = require './lib/update-lock'
|
||||
{ singleToMulticontainerApp } = require './lib/migration'
|
||||
{ ENOENT, EISDIR, NotFoundError, UpdatesLockedError } = require './lib/errors'
|
||||
|
||||
DeviceConfig = require './device-config'
|
||||
{ DeviceConfig } = require './device-config'
|
||||
ApplicationManager = require './application-manager'
|
||||
|
||||
validateLocalState = (state) ->
|
||||
@ -534,7 +534,7 @@ module.exports = class DeviceState extends EventEmitter
|
||||
|
||||
executeStepAction: (step, { force, initial, skipLock }) =>
|
||||
Promise.try =>
|
||||
if _.includes(@deviceConfig.validActions, step.action)
|
||||
if @deviceConfig.isValidAction(step.action)
|
||||
@deviceConfig.executeStepAction(step, { initial })
|
||||
else if _.includes(@applications.validActions, step.action)
|
||||
@applications.executeStepAction(step, { force, skipLock })
|
||||
|
@ -8,7 +8,7 @@ m = require 'mochainon'
|
||||
prepare = require './lib/prepare'
|
||||
fsUtils = require '../src/lib/fs-utils'
|
||||
|
||||
DeviceConfig = require '../src/device-config'
|
||||
{ DeviceConfig } = require '../src/device-config'
|
||||
{ ExtlinuxConfigBackend, RPiConfigBackend } = require '../src/config/backend'
|
||||
|
||||
extlinuxBackend = new ExtlinuxConfigBackend()
|
||||
|
Loading…
x
Reference in New Issue
Block a user