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';
|
2021-01-21 19:27:07 +00:00
|
|
|
|
import { promisify } from 'util';
|
2019-10-31 01:46:14 +00:00
|
|
|
|
import * as _ from 'lodash';
|
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-07-08 17:03:10 +00:00
|
|
|
|
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
2021-07-15 13:41:38 +00:00
|
|
|
|
import {
|
|
|
|
|
applicationIdInfo,
|
|
|
|
|
appToFleetFlagMsg,
|
|
|
|
|
warnify,
|
|
|
|
|
} from '../../utils/messages';
|
|
|
|
|
import { isV13 } from '../../utils/version';
|
2019-10-31 01:46:14 +00:00
|
|
|
|
|
2020-01-21 16:51:06 +00:00
|
|
|
|
const CONNECTIONS_FOLDER = '/system-connections';
|
|
|
|
|
|
2019-10-31 01:46:14 +00:00
|
|
|
|
interface FlagsDef {
|
|
|
|
|
advanced?: boolean;
|
|
|
|
|
application?: string;
|
2020-12-10 12:30:17 +00:00
|
|
|
|
app?: string;
|
2021-07-15 13:41:38 +00:00
|
|
|
|
fleet?: string;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
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;
|
2021-07-20 06:42:15 +00:00
|
|
|
|
'provisioning-key-name'?: string;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ArgsDef {
|
|
|
|
|
image: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2021-07-20 06:42:15 +00:00
|
|
|
|
provisioningKeyName?: string;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
2021-07-15 13:41:38 +00:00
|
|
|
|
Configure a previously downloaded balenaOS image for a specific device type
|
|
|
|
|
or fleet.
|
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).
|
|
|
|
|
|
2021-07-15 13:41:38 +00:00
|
|
|
|
The --device-type option may be used to override the fleet's default device
|
|
|
|
|
type, in case of a fleet with mixed device types.
|
2019-10-31 01:46:14 +00:00
|
|
|
|
|
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/
|
2021-03-30 09:06:30 +00:00
|
|
|
|
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
|
2020-01-21 16:51:06 +00:00
|
|
|
|
|
2019-10-31 01:46:14 +00:00
|
|
|
|
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
|
2020-06-25 11:57:47 +00:00
|
|
|
|
|
2020-12-10 12:30:17 +00:00
|
|
|
|
${applicationIdInfo.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
|
2020-10-20 21:11:17 +00:00
|
|
|
|
the Linux release of the balena CLI:
|
2020-06-25 11:57:47 +00:00
|
|
|
|
https://docs.microsoft.com/en-us/windows/wsl/about
|
2019-10-31 01:46:14 +00:00
|
|
|
|
`;
|
2020-12-10 12:30:17 +00:00
|
|
|
|
|
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>',
|
2021-07-15 13:41:38 +00:00
|
|
|
|
'$ balena os configure ../path/rpi3.img --fleet myorg/myfleet',
|
|
|
|
|
'$ balena os configure ../path/rpi3.img --fleet MyFleet --version 2.12.7',
|
|
|
|
|
'$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3',
|
|
|
|
|
'$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3 --config myWifiConfig.json',
|
2019-10-31 01:46:14 +00:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public static args = [
|
|
|
|
|
{
|
|
|
|
|
name: 'image',
|
|
|
|
|
required: true,
|
|
|
|
|
description: 'path to a balenaOS image file, e.g. "rpi3.img"',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2020-09-04 14:34:34 +00:00
|
|
|
|
public static usage = 'os configure <image>';
|
2019-10-31 01:46:14 +00:00
|
|
|
|
|
|
|
|
|
public static flags: flags.Input<FlagsDef> = {
|
|
|
|
|
advanced: flags.boolean({
|
|
|
|
|
char: 'v',
|
|
|
|
|
description:
|
|
|
|
|
'ask advanced configuration questions (when in interactive mode)',
|
|
|
|
|
}),
|
2021-07-15 13:41:38 +00:00
|
|
|
|
...(isV13()
|
|
|
|
|
? {}
|
|
|
|
|
: {
|
|
|
|
|
application: {
|
|
|
|
|
...cf.application,
|
|
|
|
|
exclusive: ['app', 'fleet', 'device'],
|
|
|
|
|
},
|
|
|
|
|
app: {
|
|
|
|
|
...cf.app,
|
|
|
|
|
exclusive: ['application', 'fleet', 'device'],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
fleet: {
|
|
|
|
|
...cf.fleet,
|
|
|
|
|
exclusive: ['app', 'application', 'device'],
|
|
|
|
|
},
|
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',
|
2021-07-20 06:42:15 +00:00
|
|
|
|
exclusive: ['provisioning-key-name'],
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}),
|
|
|
|
|
'config-app-update-poll-interval': flags.integer({
|
|
|
|
|
description:
|
2021-07-15 13:41:38 +00:00
|
|
|
|
'supervisor cloud polling interval in minutes (e.g. for variable updates)',
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}),
|
|
|
|
|
'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)',
|
|
|
|
|
}),
|
2021-07-20 06:42:15 +00:00
|
|
|
|
device: {
|
|
|
|
|
exclusive: ['app', 'application', 'fleet', 'provisioning-key-name'],
|
|
|
|
|
...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:
|
2021-07-15 13:41:38 +00:00
|
|
|
|
'device type slug (e.g. "raspberrypi3") to override the fleet device type',
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}),
|
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
|
|
|
|
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",
|
|
|
|
|
}),
|
2021-07-20 06:42:15 +00:00
|
|
|
|
'provisioning-key-name': flags.string({
|
|
|
|
|
description: 'custom key name assigned to generated provisioning api key',
|
|
|
|
|
exclusive: ['config', 'device'],
|
|
|
|
|
}),
|
2020-12-10 12:30:17 +00:00
|
|
|
|
help: cf.help,
|
2019-10-31 01:46:14 +00:00
|
|
|
|
};
|
|
|
|
|
|
2021-11-24 23:00:38 +00:00
|
|
|
|
public static authenticated = true;
|
|
|
|
|
|
2019-10-31 01:46:14 +00:00
|
|
|
|
public async run() {
|
|
|
|
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
|
|
|
|
OsConfigureCmd,
|
|
|
|
|
);
|
2021-07-15 13:41:38 +00:00
|
|
|
|
if ((options.application || options.app) && process.stderr.isTTY) {
|
|
|
|
|
console.error(warnify(appToFleetFlagMsg));
|
|
|
|
|
}
|
|
|
|
|
options.application ||= options.app || options.fleet;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
|
|
|
|
|
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-11-04 15:42:31 +00:00
|
|
|
|
const { getApplication } = await import('../../utils/sdk');
|
|
|
|
|
|
2020-07-31 14:35:20 +00:00
|
|
|
|
let app: ApplicationWithDeviceType | undefined;
|
|
|
|
|
let device;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
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) {
|
2020-07-31 14:35:20 +00:00
|
|
|
|
device = (await balena.models.device.get(options.device, {
|
|
|
|
|
$expand: {
|
|
|
|
|
is_of__device_type: { $select: 'slug' },
|
|
|
|
|
},
|
|
|
|
|
})) as DeviceWithDeviceType & {
|
|
|
|
|
belongs_to__application: BalenaSdk.PineDeferred;
|
|
|
|
|
};
|
|
|
|
|
deviceTypeSlug = device.is_of__device_type[0].slug;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
} else {
|
2020-11-04 15:42:31 +00:00
|
|
|
|
app = (await getApplication(balena, options.application!, {
|
2020-07-31 14:35:20 +00:00
|
|
|
|
$expand: {
|
|
|
|
|
is_for__device_type: { $select: 'slug' },
|
|
|
|
|
},
|
|
|
|
|
})) as ApplicationWithDeviceType;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
await checkDeviceTypeCompatibility(balena, options, app);
|
2020-07-31 14:35:20 +00:00
|
|
|
|
deviceTypeSlug =
|
|
|
|
|
options['device-type'] || app.is_for__device_type[0].slug;
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
2021-07-20 06:42:15 +00:00
|
|
|
|
answers.provisioningKeyName = options['provisioning-key-name'];
|
|
|
|
|
|
2019-10-31 01:46:14 +00:00
|
|
|
|
if (_.isEmpty(configJson)) {
|
|
|
|
|
if (device) {
|
|
|
|
|
configJson = await generateDeviceConfig(
|
2020-07-31 14:35:20 +00:00
|
|
|
|
device,
|
2019-10-31 01:46:14 +00:00
|
|
|
|
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-12-10 12:30:17 +00:00
|
|
|
|
const path = await import('path');
|
|
|
|
|
|
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
|
|
|
|
);
|
2021-01-21 19:27:07 +00:00
|
|
|
|
|
2021-11-26 23:13:31 +00:00
|
|
|
|
const { getBootPartition } = await import('balena-config-json');
|
|
|
|
|
const bootPartition = await getBootPartition(params.image);
|
2021-05-15 14:18:14 +00:00
|
|
|
|
|
2021-01-21 19:27:07 +00:00
|
|
|
|
const imagefs = await import('balena-image-fs');
|
2020-01-21 16:51:06 +00:00
|
|
|
|
|
2020-06-30 20:18:34 +00:00
|
|
|
|
for (const { name, content } of files) {
|
2021-05-15 14:18:14 +00:00
|
|
|
|
await imagefs.interact(image, bootPartition, async (_fs) => {
|
2021-01-21 19:27:07 +00:00
|
|
|
|
return await promisify(_fs.writeFile)(
|
|
|
|
|
path.join(CONNECTIONS_FOLDER, name),
|
|
|
|
|
content,
|
|
|
|
|
);
|
|
|
|
|
});
|
2020-01-21 16:51:06 +00:00
|
|
|
|
console.info(`Copied system-connection file: ${name}`);
|
2020-06-30 20:18:34 +00:00
|
|
|
|
}
|
2020-01-21 16:51:06 +00:00
|
|
|
|
}
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateOptions(options: FlagsDef) {
|
|
|
|
|
// 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(
|
2021-07-15 13:41:38 +00:00
|
|
|
|
"Either the '--device' or the '--fleet' option must be provided",
|
2019-10-31 01:46:14 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (!options.application && options['device-type']) {
|
|
|
|
|
throw new ExpectedError(
|
2021-07-15 13:41:38 +00:00
|
|
|
|
"The '--device-type' option can only be used in conjunction with the '--fleet' option",
|
2019-10-31 01:46:14 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
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,
|
2020-07-31 14:35:20 +00:00
|
|
|
|
deviceTypeManifest: BalenaSdk.DeviceTypeJson.DeviceType,
|
2019-10-31 01:46:14 +00:00
|
|
|
|
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,
|
2020-07-31 14:35:20 +00:00
|
|
|
|
app: ApplicationWithDeviceType,
|
2019-10-31 01:46:14 +00:00
|
|
|
|
) {
|
|
|
|
|
if (options['device-type']) {
|
|
|
|
|
const [appDeviceType, optionDeviceType] = await Promise.all([
|
2020-07-31 14:35:20 +00:00
|
|
|
|
sdk.models.device.getManifestBySlug(app.is_for__device_type[0].slug),
|
2019-10-31 01:46:14 +00:00
|
|
|
|
sdk.models.device.getManifestBySlug(options['device-type']),
|
|
|
|
|
]);
|
|
|
|
|
const helpers = await import('../../utils/helpers');
|
|
|
|
|
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
|
|
|
|
|
throw new ExpectedError(
|
2021-07-15 13:41:38 +00:00
|
|
|
|
`Device type ${options['device-type']} is incompatible with fleet ${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(
|
2020-07-31 14:35:20 +00:00
|
|
|
|
deviceType: BalenaSdk.DeviceTypeJson.DeviceType,
|
2019-10-31 01:46:14 +00:00
|
|
|
|
options: FlagsDef,
|
|
|
|
|
configJson?: import('../../utils/config').ImgConfig,
|
|
|
|
|
): Promise<Answers> {
|
|
|
|
|
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)) {
|
2020-07-08 17:21:37 +00:00
|
|
|
|
const helpers = await import('../../utils/helpers');
|
2019-10-31 01:46:14 +00:00
|
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-08 17:03:10 +00:00
|
|
|
|
return getCliForm().run(questions, extraOpts);
|
2019-10-31 01:46:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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(
|
2020-07-31 14:35:20 +00:00
|
|
|
|
deviceType: BalenaSdk.DeviceTypeJson.DeviceType,
|
2019-10-31 01:46:14 +00:00
|
|
|
|
): Array<keyof Answers> {
|
|
|
|
|
const questionNames: string[] = _.chain(deviceType.options)
|
|
|
|
|
.flatMap(
|
2020-07-31 14:35:20 +00:00
|
|
|
|
(group: BalenaSdk.DeviceTypeJson.DeviceTypeOptions) =>
|
2019-10-31 01:46:14 +00:00
|
|
|
|
(group.isGroup && group.options) || [],
|
|
|
|
|
)
|
2020-07-31 14:35:20 +00:00
|
|
|
|
.map(
|
|
|
|
|
(groupOption: BalenaSdk.DeviceTypeJson.DeviceTypeOptionsGroup) =>
|
|
|
|
|
groupOption.name,
|
|
|
|
|
)
|
2019-10-31 01:46:14 +00:00
|
|
|
|
.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;
|
|
|
|
|
});
|
|
|
|
|
}
|