2019-10-31 01:46:14 +00:00
|
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2019 Balena Ltd.
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
2020-03-19 08:45:57 +00:00
|
|
|
|
import { flags } from '@oclif/command';
|
2020-02-27 12:38:50 +00:00
|
|
|
|
import type * as BalenaSdk from 'balena-sdk';
|
2020-01-21 16:51:06 +00:00
|
|
|
|
import Bluebird = require('bluebird');
|
2019-10-31 01:46:14 +00:00
|
|
|
|
import * as _ from 'lodash';
|
2020-01-21 16:51:06 +00:00
|
|
|
|
import * as path from 'path';
|
2020-03-19 08:45:57 +00:00
|
|
|
|
import Command from '../../command';
|
2019-10-31 01:46:14 +00:00
|
|
|
|
|
|
|
|
|
import { ExpectedError } from '../../errors';
|
|
|
|
|
import * as cf from '../../utils/common-flags';
|
2020-05-04 14:57:23 +00:00
|
|
|
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
2019-10-31 01:46:14 +00:00
|
|
|
|
import { CommandHelp } from '../../utils/oclif-utils';
|
|
|
|
|
|
2020-01-21 16:51:06 +00:00
|
|
|
|
const BOOT_PARTITION = 1;
|
|
|
|
|
const CONNECTIONS_FOLDER = '/system-connections';
|
|
|
|
|
|
2019-10-31 01:46:14 +00:00
|
|
|
|
interface FlagsDef {
|
|
|
|
|
advanced?: boolean;
|
|
|
|
|
app?: string;
|
|
|
|
|
application?: string;
|
|
|
|
|
config?: string;
|
|
|
|
|
'config-app-update-poll-interval'?: number;
|
|
|
|
|
'config-network'?: string;
|
|
|
|
|
'config-wifi-key'?: string;
|
|
|
|
|
'config-wifi-ssid'?: string;
|
|
|
|
|
device?: string; // device UUID
|
|
|
|
|
'device-api-key'?: string;
|
|
|
|
|
'device-type'?: string;
|
|
|
|
|
help?: void;
|
|
|
|
|
version?: string;
|
2020-01-21 16:51:06 +00:00
|
|
|
|
'system-connection': string[];
|
2020-06-11 11:32:20 +00:00
|
|
|
|
'initial-device-name'?: string;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ArgsDef {
|
|
|
|
|
image: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DeferredDevice extends BalenaSdk.Device {
|
|
|
|
|
belongs_to__application: BalenaSdk.PineDeferred;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Answers {
|
|
|
|
|
appUpdatePollInterval: number; // in minutes
|
|
|
|
|
deviceType: string; // e.g. "raspberrypi3"
|
|
|
|
|
network: 'ethernet' | 'wifi';
|
|
|
|
|
version: string; // e.g. "2.32.0+rev1"
|
|
|
|
|
wifiSsid?: string;
|
|
|
|
|
wifiKey?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deviceApiKeyDeprecationMsg = stripIndent`
|
|
|
|
|
The --device-api-key option is deprecated and will be removed in a future release.
|
|
|
|
|
A suitable key is automatically generated or fetched if this option is omitted.`;
|
|
|
|
|
|
|
|
|
|
export default class OsConfigureCmd extends Command {
|
|
|
|
|
public static description = stripIndent`
|
|
|
|
|
Configure a previously downloaded balenaOS image.
|
|
|
|
|
|
|
|
|
|
Configure a previously downloaded balenaOS image for a specific device type or
|
|
|
|
|
balena application.
|
2020-01-21 16:51:06 +00:00
|
|
|
|
|
2019-10-31 01:46:14 +00:00
|
|
|
|
Configuration settings such as WiFi authentication will be taken from the
|
|
|
|
|
following sources, in precedence order:
|
|
|
|
|
1. Command-line options like \`--config-wifi-ssid\`
|
|
|
|
|
2. A given \`config.json\` file specified with the \`--config\` option.
|
|
|
|
|
3. User input through interactive prompts (text menus).
|
|
|
|
|
|
|
|
|
|
The --device-type option may be used to override the application's default
|
|
|
|
|
device type, in case of an application with mixed device types.
|
|
|
|
|
|
2020-01-21 16:51:06 +00:00
|
|
|
|
The --system-connection (-c) option can be used to inject NetworkManager connection
|
|
|
|
|
profiles for additional network interfaces, such as cellular/GSM or additional
|
|
|
|
|
WiFi or ethernet connections. This option may be passed multiple times in case there
|
|
|
|
|
are multiple files to inject. See connection profile examples and reference at:
|
|
|
|
|
https://www.balena.io/docs/reference/OS/network/2.x/
|
|
|
|
|
https://developer.gnome.org/NetworkManager/stable/nm-settings.html
|
|
|
|
|
|
2019-10-31 01:46:14 +00:00
|
|
|
|
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
|
2020-06-25 11:57:47 +00:00
|
|
|
|
|
|
|
|
|
Note: This command is currently not supported on Windows natively. Windows users
|
|
|
|
|
are advised to install the Windows Subsystem for Linux (WSL) with Ubuntu, and use
|
|
|
|
|
the Linux release of the balena CLI:
|
|
|
|
|
https://docs.microsoft.com/en-us/windows/wsl/about
|
2019-10-31 01:46:14 +00:00
|
|
|
|
`;
|
|
|
|
|
public static examples = [
|
|
|
|
|
'$ balena os configure ../path/rpi3.img --device 7cf02a6',
|
|
|
|
|
'$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>',
|
|
|
|
|
'$ balena os configure ../path/rpi3.img --app MyApp',
|
|
|
|
|
'$ balena os configure ../path/rpi3.img --app MyApp --version 2.12.7',
|
|
|
|
|
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3',
|
|
|
|
|
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3 --config myWifiConfig.json',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public static args = [
|
|
|
|
|
{
|
|
|
|
|
name: 'image',
|
|
|
|
|
required: true,
|
|
|
|
|
description: 'path to a balenaOS image file, e.g. "rpi3.img"',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// hardcoded 'os configure' to avoid oclif's 'os:configure' topic syntax
|
|
|
|
|
public static usage =
|
|
|
|
|
'os configure ' +
|
|
|
|
|
new CommandHelp({ args: OsConfigureCmd.args }).defaultUsage();
|
|
|
|
|
|
|
|
|
|
public static flags: flags.Input<FlagsDef> = {
|
|
|
|
|
advanced: flags.boolean({
|
|
|
|
|
char: 'v',
|
|
|
|
|
description:
|
|
|
|
|
'ask advanced configuration questions (when in interactive mode)',
|
|
|
|
|
}),
|
|
|
|
|
app: flags.string({
|
|
|
|
|
description: "same as '--application'",
|
|
|
|
|
exclusive: ['application', 'device'],
|
|
|
|
|
}),
|
2020-02-29 22:06:54 +00:00
|
|
|
|
application: { exclusive: ['app', 'device'], ...cf.application },
|
2019-10-31 01:46:14 +00:00
|
|
|
|
config: flags.string({
|
|
|
|
|
description:
|
|
|
|
|
'path to a pre-generated config.json file to be injected in the OS image',
|
|
|
|
|
}),
|
|
|
|
|
'config-app-update-poll-interval': flags.integer({
|
|
|
|
|
description:
|
|
|
|
|
'interval (in minutes) for the on-device balena supervisor periodic app update check',
|
|
|
|
|
}),
|
|
|
|
|
'config-network': flags.string({
|
|
|
|
|
description: 'device network type (non-interactive configuration)',
|
|
|
|
|
options: ['ethernet', 'wifi'],
|
|
|
|
|
}),
|
|
|
|
|
'config-wifi-key': flags.string({
|
|
|
|
|
description: 'WiFi key (password) (non-interactive configuration)',
|
|
|
|
|
}),
|
|
|
|
|
'config-wifi-ssid': flags.string({
|
|
|
|
|
description: 'WiFi SSID (network name) (non-interactive configuration)',
|
|
|
|
|
}),
|
2020-02-29 22:06:54 +00:00
|
|
|
|
device: { exclusive: ['app', 'application'], ...cf.device },
|
2019-10-31 01:46:14 +00:00
|
|
|
|
'device-api-key': flags.string({
|
|
|
|
|
char: 'k',
|
|
|
|
|
description:
|
|
|
|
|
'custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)',
|
|
|
|
|
}),
|
|
|
|
|
'device-type': flags.string({
|
|
|
|
|
description:
|
|
|
|
|
'device type slug (e.g. "raspberrypi3") to override the application device type',
|
|
|
|
|
}),
|
2020-06-11 11:32:20 +00:00
|
|
|
|
'initial-device-name': flags.string({
|
|
|
|
|
description:
|
|
|
|
|
'This option will set the device name when the device provisions',
|
|
|
|
|
}),
|
2019-10-31 01:46:14 +00:00
|
|
|
|
help: cf.help,
|
|
|
|
|
version: flags.string({
|
|
|
|
|
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
|
|
|
|
}),
|
2020-01-21 16:51:06 +00:00
|
|
|
|
'system-connection': flags.string({
|
|
|
|
|
multiple: true,
|
|
|
|
|
char: 'c',
|
|
|
|
|
required: false,
|
|
|
|
|
description:
|
|
|
|
|
"paths to local files to place into the 'system-connections' directory",
|
|
|
|
|
}),
|
2019-10-31 01:46:14 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public async run() {
|
|
|
|
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
|
|
|
|
OsConfigureCmd,
|
|
|
|
|
);
|
|
|
|
|
// Prefer options.application over options.app
|
|
|
|
|
options.application = options.application || options.app;
|
|
|
|
|
options.app = undefined;
|
|
|
|
|
|
|
|
|
|
await validateOptions(options);
|
|
|
|
|
|
|
|
|
|
const devInit = await import('balena-device-init');
|
2020-06-23 10:57:57 +00:00
|
|
|
|
const { promises: fs } = await import('fs');
|
2019-10-31 01:46:14 +00:00
|
|
|
|
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
|
|
|
|
'../../utils/config'
|
|
|
|
|
);
|
|
|
|
|
const helpers = await import('../../utils/helpers');
|
2020-01-21 16:51:06 +00:00
|
|
|
|
const imagefs = await require('resin-image-fs');
|
2019-10-31 01:46:14 +00:00
|
|
|
|
let app: BalenaSdk.Application | undefined;
|
|
|
|
|
let device: BalenaSdk.Device | undefined;
|
|
|
|
|
let deviceTypeSlug: string;
|
|
|
|
|
|
2020-02-27 14:55:30 +00:00
|
|
|
|
const balena = getBalenaSdk();
|
2019-10-31 01:46:14 +00:00
|
|
|
|
if (options.device) {
|
|
|
|
|
device = await balena.models['device'].get(options.device);
|
|
|
|
|
deviceTypeSlug = device.device_type;
|
|
|
|
|
} else {
|
|
|
|
|
app = await balena.models['application'].get(options.application!);
|
|
|
|
|
await checkDeviceTypeCompatibility(balena, options, app);
|
|
|
|
|
deviceTypeSlug = options['device-type'] || app.device_type;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deviceTypeManifest = await helpers.getManifest(
|
|
|
|
|
params.image,
|
|
|
|
|
deviceTypeSlug,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let configJson: import('../../utils/config').ImgConfig | undefined;
|
|
|
|
|
if (options.config) {
|
|
|
|
|
const rawConfig = await fs.readFile(options.config, 'utf8');
|
|
|
|
|
configJson = JSON.parse(rawConfig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const answers: Answers = await askQuestionsForDeviceType(
|
|
|
|
|
deviceTypeManifest,
|
|
|
|
|
options,
|
|
|
|
|
configJson,
|
|
|
|
|
);
|
|
|
|
|
if (options.application) {
|
|
|
|
|
answers.deviceType = deviceTypeSlug;
|
|
|
|
|
}
|
|
|
|
|
answers.version =
|
|
|
|
|
options.version ||
|
|
|
|
|
(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit));
|
|
|
|
|
|
|
|
|
|
if (_.isEmpty(configJson)) {
|
|
|
|
|
if (device) {
|
|
|
|
|
configJson = await generateDeviceConfig(
|
|
|
|
|
device as DeferredDevice,
|
|
|
|
|
options['device-api-key'],
|
|
|
|
|
answers,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
configJson = await generateApplicationConfig(app!, answers);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-11 11:32:20 +00:00
|
|
|
|
if (
|
|
|
|
|
options['initial-device-name'] &&
|
|
|
|
|
options['initial-device-name'] !== ''
|
|
|
|
|
) {
|
|
|
|
|
configJson!.initialDeviceName = options['initial-device-name'];
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-31 01:46:14 +00:00
|
|
|
|
console.info('Configuring operating system image');
|
|
|
|
|
|
2020-01-21 16:51:06 +00:00
|
|
|
|
const image = params.image;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
await helpers.osProgressHandler(
|
|
|
|
|
await devInit.configure(
|
2020-01-21 16:51:06 +00:00
|
|
|
|
image,
|
2019-10-31 01:46:14 +00:00
|
|
|
|
deviceTypeManifest,
|
|
|
|
|
configJson || {},
|
|
|
|
|
answers,
|
|
|
|
|
),
|
|
|
|
|
);
|
2020-01-21 16:51:06 +00:00
|
|
|
|
|
|
|
|
|
if (options['system-connection']) {
|
2020-06-30 20:17:24 +00:00
|
|
|
|
const files = await Promise.all(
|
|
|
|
|
options['system-connection'].map(async (filePath) => {
|
2020-01-21 16:51:06 +00:00
|
|
|
|
const content = await fs.readFile(filePath, 'utf8');
|
|
|
|
|
const name = path.basename(filePath);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
content,
|
|
|
|
|
};
|
2020-06-30 20:17:24 +00:00
|
|
|
|
}),
|
2020-01-21 16:51:06 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await Bluebird.each(files, async ({ name, content }) => {
|
|
|
|
|
await imagefs.writeFile(
|
|
|
|
|
{
|
|
|
|
|
image,
|
|
|
|
|
partition: BOOT_PARTITION,
|
|
|
|
|
path: path.join(CONNECTIONS_FOLDER, name),
|
|
|
|
|
},
|
|
|
|
|
content,
|
|
|
|
|
);
|
|
|
|
|
console.info(`Copied system-connection file: ${name}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateOptions(options: FlagsDef) {
|
|
|
|
|
if (process.platform === 'win32') {
|
|
|
|
|
throw new ExpectedError(stripIndent`
|
|
|
|
|
Unsupported platform error: the 'balena os configure' command currently requires
|
|
|
|
|
the Windows Subsystem for Linux in order to run on Windows. It was tested with
|
|
|
|
|
the Ubuntu 18.04 distribution from the Microsoft Store. With WSL, a balena CLI
|
|
|
|
|
release for Linux (rather than Windows) should be installed: for example, the
|
|
|
|
|
standalone zip package for Linux. (It is possible to have both a Windows CLI
|
|
|
|
|
release and a Linux CLI release installed simultaneously.) For more information
|
|
|
|
|
on WSL and the balena CLI installation options, please check:
|
|
|
|
|
- https://docs.microsoft.com/en-us/windows/wsl/about
|
|
|
|
|
- https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
|
|
|
|
`);
|
|
|
|
|
}
|
|
|
|
|
// The 'device' and 'application' options are declared "exclusive" in the oclif
|
|
|
|
|
// flag definitions above, so oclif will enforce that they are not both used together.
|
|
|
|
|
if (!options.device && !options.application) {
|
|
|
|
|
throw new ExpectedError(
|
|
|
|
|
"Either the '--device' or the '--application' option must be provided",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (!options.application && options['device-type']) {
|
|
|
|
|
throw new ExpectedError(
|
|
|
|
|
"The '--device-type' option can only be used in conjunction with the '--application' option",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (options['device-api-key']) {
|
|
|
|
|
console.error(stripIndent`
|
|
|
|
|
-------------------------------------------------------------------------------------------
|
|
|
|
|
Warning: ${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t\t')}
|
|
|
|
|
-------------------------------------------------------------------------------------------
|
|
|
|
|
`);
|
|
|
|
|
}
|
2020-03-24 08:51:19 +00:00
|
|
|
|
|
|
|
|
|
await Command.checkLoggedIn();
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Wrapper around balena-device-init.getImageOsVersion(). Throws ExpectedError
|
|
|
|
|
* if the OS image could not be read or the OS version could not be extracted
|
|
|
|
|
* from it.
|
|
|
|
|
* @param imagePath Local filesystem path to a balenaOS image file
|
|
|
|
|
* @param deviceTypeManifest Device type manifest object
|
|
|
|
|
*/
|
|
|
|
|
async function getOsVersionFromImage(
|
|
|
|
|
imagePath: string,
|
|
|
|
|
deviceTypeManifest: BalenaSdk.DeviceType,
|
|
|
|
|
devInit: typeof import('balena-device-init'),
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const osVersion = await devInit.getImageOsVersion(
|
|
|
|
|
imagePath,
|
|
|
|
|
deviceTypeManifest,
|
|
|
|
|
);
|
|
|
|
|
if (!osVersion) {
|
|
|
|
|
throw new ExpectedError(stripIndent`
|
|
|
|
|
Could not read OS version from the image. Please specify the balenaOS
|
|
|
|
|
version manually with the --version command-line option.`);
|
|
|
|
|
}
|
|
|
|
|
return osVersion;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check that options['device-type'], e.g. 'raspberrypi3', is compatible with
|
|
|
|
|
* app.device_type, e.g. 'raspberry-pi2'. Throws ExpectedError if they are not
|
|
|
|
|
* compatible.
|
|
|
|
|
* @param sdk Balena Node SDK instance
|
|
|
|
|
* @param options oclif command-line options object
|
|
|
|
|
* @param app Balena SDK Application model object
|
|
|
|
|
*/
|
|
|
|
|
async function checkDeviceTypeCompatibility(
|
|
|
|
|
sdk: BalenaSdk.BalenaSDK,
|
|
|
|
|
options: FlagsDef,
|
|
|
|
|
app: BalenaSdk.Application,
|
|
|
|
|
) {
|
|
|
|
|
if (options['device-type']) {
|
|
|
|
|
const [appDeviceType, optionDeviceType] = await Promise.all([
|
|
|
|
|
sdk.models.device.getManifestBySlug(app.device_type),
|
|
|
|
|
sdk.models.device.getManifestBySlug(options['device-type']),
|
|
|
|
|
]);
|
|
|
|
|
const helpers = await import('../../utils/helpers');
|
|
|
|
|
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
|
|
|
|
|
throw new ExpectedError(
|
2020-01-20 21:21:05 +00:00
|
|
|
|
`Device type ${options['device-type']} is incompatible with application ${options.application}`,
|
2019-10-31 01:46:14 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if the given options or configJson objects (in this order) contain
|
|
|
|
|
* the answers to some configuration questions, and interactively ask the
|
|
|
|
|
* user the questions for which answers are missing. Questions such as:
|
|
|
|
|
*
|
|
|
|
|
* ? Network Connection (Use arrow keys)
|
|
|
|
|
* ethernet
|
|
|
|
|
* ❯ wifi
|
|
|
|
|
* ? Network Connection wifi
|
|
|
|
|
* ? Wifi SSID i-ssid
|
|
|
|
|
* ? Wifi Passphrase [input is hidden]
|
|
|
|
|
*
|
|
|
|
|
* The questions are extracted from the given deviceType "manifest".
|
|
|
|
|
*/
|
|
|
|
|
async function askQuestionsForDeviceType(
|
|
|
|
|
deviceType: BalenaSdk.DeviceType,
|
|
|
|
|
options: FlagsDef,
|
|
|
|
|
configJson?: import('../../utils/config').ImgConfig,
|
|
|
|
|
): Promise<Answers> {
|
|
|
|
|
const form = await import('resin-cli-form');
|
|
|
|
|
const helpers = await import('../../utils/helpers');
|
|
|
|
|
const answerSources: any[] = [camelifyConfigOptions(options)];
|
|
|
|
|
const defaultAnswers: Partial<Answers> = {};
|
|
|
|
|
const questions: any = deviceType.options;
|
|
|
|
|
let extraOpts: { override: object } | undefined;
|
|
|
|
|
|
|
|
|
|
if (!_.isEmpty(configJson)) {
|
|
|
|
|
answerSources.push(configJson);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!options.advanced) {
|
|
|
|
|
const advancedGroup: any = _.find(questions, {
|
|
|
|
|
name: 'advanced',
|
|
|
|
|
isGroup: true,
|
|
|
|
|
});
|
|
|
|
|
if (!_.isEmpty(advancedGroup)) {
|
|
|
|
|
answerSources.push(helpers.getGroupDefaults(advancedGroup));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const questionName of getQuestionNames(deviceType)) {
|
|
|
|
|
for (const answerSource of answerSources) {
|
|
|
|
|
if (answerSource[questionName] != null) {
|
|
|
|
|
defaultAnswers[questionName] = answerSource[questionName];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
!defaultAnswers.network &&
|
|
|
|
|
(defaultAnswers.wifiSsid || defaultAnswers.wifiKey)
|
|
|
|
|
) {
|
|
|
|
|
defaultAnswers.network = 'wifi';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!_.isEmpty(defaultAnswers)) {
|
|
|
|
|
extraOpts = { override: defaultAnswers };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return form.run(questions, extraOpts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Given a deviceType "manifest" containing "options" properties, return an
|
|
|
|
|
* array of "question names" as in the following example.
|
|
|
|
|
*
|
|
|
|
|
* @param deviceType Device type "manifest", for example:
|
|
|
|
|
* { "slug": "raspberrypi3",
|
|
|
|
|
* "options": [{
|
|
|
|
|
* "options": [ {
|
|
|
|
|
* "name": "network",
|
|
|
|
|
* "choices": ["ethernet", "wifi"],
|
|
|
|
|
* ... }, {
|
|
|
|
|
* "name": "wifiSsid",
|
|
|
|
|
* "type": "text",
|
|
|
|
|
* ... }, {
|
|
|
|
|
* "options": [ {
|
|
|
|
|
* "name": "appUpdatePollInterval",
|
|
|
|
|
* "default": 10,
|
|
|
|
|
* ...
|
|
|
|
|
* @return Array of question names, for example:
|
|
|
|
|
* [ 'network', 'wifiSsid', 'wifiKey', 'appUpdatePollInterval' ]
|
|
|
|
|
*/
|
|
|
|
|
function getQuestionNames(
|
|
|
|
|
deviceType: BalenaSdk.DeviceType,
|
|
|
|
|
): Array<keyof Answers> {
|
|
|
|
|
const questionNames: string[] = _.chain(deviceType.options)
|
|
|
|
|
.flatMap(
|
|
|
|
|
(group: BalenaSdk.DeviceTypeOptions) =>
|
|
|
|
|
(group.isGroup && group.options) || [],
|
|
|
|
|
)
|
|
|
|
|
.map((groupOption: BalenaSdk.DeviceTypeOptionsGroup) => groupOption.name)
|
|
|
|
|
.filter()
|
|
|
|
|
.value();
|
|
|
|
|
return questionNames as Array<keyof Answers>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create and return a new object with the key-value pairs from the input object,
|
|
|
|
|
* renaming keys that start with the 'config-' prefix as follows:
|
|
|
|
|
* Sample input:
|
|
|
|
|
* { app: 'foo', 'config-wifi-key': 'mykey', 'config-wifi-ssid': 'myssid' }
|
|
|
|
|
* Output:
|
|
|
|
|
* { app: 'foo', wifiKey: 'mykey', wifiSsid: 'myssid' }
|
|
|
|
|
*/
|
|
|
|
|
function camelifyConfigOptions(options: FlagsDef): { [key: string]: any } {
|
|
|
|
|
return _.mapKeys(options, (_value, key) => {
|
|
|
|
|
if (key.startsWith('config-')) {
|
|
|
|
|
return key
|
|
|
|
|
.substring('config-'.length)
|
2020-06-15 22:53:07 +00:00
|
|
|
|
.replace(/-[a-z]/g, (match) => match.substring(1).toUpperCase());
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}
|
|
|
|
|
return key;
|
|
|
|
|
});
|
|
|
|
|
}
|