2020-07-28 12:40:47 +00:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright 2016-2020 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { flags } from '@oclif/command';
|
|
|
|
import Command from '../../command';
|
|
|
|
import * as cf from '../../utils/common-flags';
|
|
|
|
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
2023-05-12 14:02:32 +00:00
|
|
|
import {
|
|
|
|
applicationIdInfo,
|
|
|
|
devModeInfo,
|
|
|
|
secureBootInfo,
|
|
|
|
} from '../../utils/messages';
|
2023-05-19 17:59:24 +00:00
|
|
|
import type { BalenaSDK, PineDeferred } from 'balena-sdk';
|
2020-07-28 12:40:47 +00:00
|
|
|
|
|
|
|
interface FlagsDef {
|
|
|
|
version: string; // OS version
|
2021-07-15 13:41:38 +00:00
|
|
|
fleet?: string;
|
2021-12-24 15:10:59 +00:00
|
|
|
dev?: boolean; // balenaOS development variant
|
2023-05-12 14:02:32 +00:00
|
|
|
secureBoot?: boolean;
|
2020-07-28 12:40:47 +00:00
|
|
|
device?: string;
|
|
|
|
deviceApiKey?: string;
|
|
|
|
deviceType?: string;
|
|
|
|
'generate-device-api-key': boolean;
|
|
|
|
output?: string;
|
|
|
|
// Options for non-interactive configuration
|
|
|
|
network?: string;
|
|
|
|
wifiSsid?: string;
|
|
|
|
wifiKey?: string;
|
|
|
|
appUpdatePollInterval?: string;
|
2021-07-20 06:42:15 +00:00
|
|
|
'provisioning-key-name'?: string;
|
2022-05-10 18:02:45 +00:00
|
|
|
'provisioning-key-expiry-date'?: string;
|
2020-07-28 12:40:47 +00:00
|
|
|
help: void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class ConfigGenerateCmd extends Command {
|
|
|
|
public static description = stripIndent`
|
|
|
|
Generate a config.json file.
|
|
|
|
|
2021-07-15 13:41:38 +00:00
|
|
|
Generate a config.json file for a device or fleet.
|
2020-07-28 12:40:47 +00:00
|
|
|
|
2021-07-15 13:41:38 +00:00
|
|
|
The target balenaOS version must be specified with the --version option.
|
2020-07-28 12:40:47 +00:00
|
|
|
|
2021-12-24 15:10:59 +00:00
|
|
|
${devModeInfo.split('\n').join('\n\t\t')}
|
|
|
|
|
2023-05-12 14:02:32 +00:00
|
|
|
${secureBootInfo.split('\n').join('\n\t\t')}
|
|
|
|
|
2021-07-15 13:41:38 +00:00
|
|
|
To configure an image for a fleet of mixed device types, use the --fleet option
|
|
|
|
alongside the --deviceType option to specify the target device type.
|
2020-07-28 12:40:47 +00:00
|
|
|
|
2021-07-15 13:41:38 +00:00
|
|
|
To avoid interactive questions, specify a command line option for each question that
|
|
|
|
would otherwise be asked.
|
2020-12-10 12:30:17 +00:00
|
|
|
|
|
|
|
${applicationIdInfo.split('\n').join('\n\t\t')}
|
2020-07-28 12:40:47 +00:00
|
|
|
`;
|
|
|
|
|
|
|
|
public static examples = [
|
|
|
|
'$ balena config generate --device 7cf02a6 --version 2.12.7',
|
|
|
|
'$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key',
|
2021-11-25 16:05:37 +00:00
|
|
|
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
|
2020-07-28 12:40:47 +00:00
|
|
|
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
2021-12-24 15:10:59 +00:00
|
|
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
|
2023-05-12 14:02:32 +00:00
|
|
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot',
|
2021-12-24 15:10:59 +00:00
|
|
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
|
|
|
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
|
|
|
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
2020-07-28 12:40:47 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
public static usage = 'config generate';
|
|
|
|
|
|
|
|
public static flags: flags.Input<FlagsDef> = {
|
|
|
|
version: flags.string({
|
|
|
|
description: 'a balenaOS version',
|
|
|
|
required: true,
|
|
|
|
}),
|
2021-12-19 20:50:03 +00:00
|
|
|
fleet: { ...cf.fleet, exclusive: ['device'] },
|
2021-12-24 15:10:59 +00:00
|
|
|
dev: cf.dev,
|
2023-05-12 14:02:32 +00:00
|
|
|
secureBoot: cf.secureBoot,
|
2021-07-20 06:42:15 +00:00
|
|
|
device: {
|
|
|
|
...cf.device,
|
2022-05-10 18:02:45 +00:00
|
|
|
exclusive: [
|
|
|
|
'fleet',
|
|
|
|
'provisioning-key-name',
|
|
|
|
'provisioning-key-expiry-date',
|
|
|
|
],
|
2021-07-20 06:42:15 +00:00
|
|
|
},
|
2020-07-28 12:40:47 +00:00
|
|
|
deviceApiKey: flags.string({
|
|
|
|
description:
|
|
|
|
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
|
|
|
char: 'k',
|
|
|
|
}),
|
|
|
|
deviceType: flags.string({
|
2021-07-15 13:41:38 +00:00
|
|
|
description:
|
|
|
|
"device type slug (run 'balena devices supported' for possible values)",
|
2020-07-28 12:40:47 +00:00
|
|
|
}),
|
|
|
|
'generate-device-api-key': flags.boolean({
|
|
|
|
description: 'generate a fresh device key for the device',
|
|
|
|
}),
|
|
|
|
output: flags.string({
|
|
|
|
description: 'path of output file',
|
|
|
|
char: 'o',
|
|
|
|
}),
|
|
|
|
// Options for non-interactive configuration
|
|
|
|
network: flags.string({
|
|
|
|
description: 'the network type to use: ethernet or wifi',
|
|
|
|
options: ['ethernet', 'wifi'],
|
|
|
|
}),
|
|
|
|
wifiSsid: flags.string({
|
|
|
|
description:
|
|
|
|
'the wifi ssid to use (used only if --network is set to wifi)',
|
|
|
|
}),
|
|
|
|
wifiKey: flags.string({
|
|
|
|
description:
|
|
|
|
'the wifi key to use (used only if --network is set to wifi)',
|
|
|
|
}),
|
|
|
|
appUpdatePollInterval: flags.string({
|
|
|
|
description:
|
2021-12-19 20:50:03 +00:00
|
|
|
'supervisor cloud polling interval in minutes (e.g. for device variables)',
|
2020-07-28 12:40:47 +00:00
|
|
|
}),
|
2021-07-20 06:42:15 +00:00
|
|
|
'provisioning-key-name': flags.string({
|
|
|
|
description: 'custom key name assigned to generated provisioning api key',
|
|
|
|
exclusive: ['device'],
|
|
|
|
}),
|
2022-05-10 18:02:45 +00:00
|
|
|
'provisioning-key-expiry-date': flags.string({
|
|
|
|
description:
|
|
|
|
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
|
|
|
exclusive: ['device'],
|
|
|
|
}),
|
2020-07-28 12:40:47 +00:00
|
|
|
help: cf.help,
|
|
|
|
};
|
|
|
|
|
|
|
|
public static authenticated = true;
|
|
|
|
|
2023-05-19 17:59:24 +00:00
|
|
|
public async getApplication(balena: BalenaSDK, fleet: string) {
|
2020-11-04 15:42:31 +00:00
|
|
|
const { getApplication } = await import('../../utils/sdk');
|
2023-05-19 17:59:24 +00:00
|
|
|
return await getApplication(balena, fleet, {
|
2023-05-19 21:30:03 +00:00
|
|
|
$select: 'slug',
|
2023-05-19 17:59:24 +00:00
|
|
|
$expand: {
|
|
|
|
is_for__device_type: { $select: 'slug' },
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2020-11-04 15:42:31 +00:00
|
|
|
|
2023-05-19 17:59:24 +00:00
|
|
|
public async run() {
|
|
|
|
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
|
2020-07-28 12:40:47 +00:00
|
|
|
const balena = getBalenaSdk();
|
|
|
|
|
|
|
|
await this.validateOptions(options);
|
|
|
|
|
2020-10-25 16:40:11 +00:00
|
|
|
let resourceDeviceType: string;
|
2023-05-19 17:59:24 +00:00
|
|
|
let application: Awaited<ReturnType<typeof this.getApplication>> | null =
|
|
|
|
null;
|
2020-11-05 13:15:55 +00:00
|
|
|
let device:
|
|
|
|
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
|
|
|
| null = null;
|
2020-07-28 12:40:47 +00:00
|
|
|
if (options.device != null) {
|
2022-07-28 16:58:26 +00:00
|
|
|
const rawDevice = await balena.models.device.get(options.device, {
|
|
|
|
$expand: { is_of__device_type: { $select: 'slug' } },
|
|
|
|
});
|
2020-11-05 13:15:55 +00:00
|
|
|
if (!rawDevice.belongs_to__application) {
|
|
|
|
const { ExpectedError } = await import('../../errors');
|
|
|
|
throw new ExpectedError(stripIndent`
|
2021-07-15 13:41:38 +00:00
|
|
|
Device ${options.device} does not appear to belong to an accessible fleet.
|
|
|
|
Try with a different device, or use '--fleet' instead of '--device'.`);
|
2020-11-05 13:15:55 +00:00
|
|
|
}
|
|
|
|
device = rawDevice as DeviceWithDeviceType & {
|
|
|
|
belongs_to__application: PineDeferred;
|
|
|
|
};
|
|
|
|
resourceDeviceType = device.is_of__device_type[0].slug;
|
2020-07-28 12:40:47 +00:00
|
|
|
} else {
|
2020-12-10 12:30:17 +00:00
|
|
|
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
2023-05-19 17:59:24 +00:00
|
|
|
application = await this.getApplication(balena, options.fleet!);
|
2020-11-05 13:15:55 +00:00
|
|
|
resourceDeviceType = application.is_for__device_type[0].slug;
|
2020-07-28 12:40:47 +00:00
|
|
|
}
|
|
|
|
|
2020-10-25 16:40:11 +00:00
|
|
|
const deviceType = options.deviceType || resourceDeviceType;
|
|
|
|
|
2020-07-28 12:40:47 +00:00
|
|
|
// Check compatibility if application and deviceType provided
|
2021-12-19 20:50:03 +00:00
|
|
|
if (options.fleet && options.deviceType) {
|
2020-07-28 12:40:47 +00:00
|
|
|
const helpers = await import('../../utils/helpers');
|
|
|
|
if (
|
2022-10-17 14:00:29 +00:00
|
|
|
!(await helpers.areDeviceTypesCompatible(
|
|
|
|
resourceDeviceType,
|
|
|
|
deviceType,
|
|
|
|
))
|
2020-07-28 12:40:47 +00:00
|
|
|
) {
|
2022-10-17 16:13:14 +00:00
|
|
|
const { ExpectedError } = await import('../../errors');
|
|
|
|
throw new ExpectedError(
|
2021-12-19 20:50:03 +00:00
|
|
|
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
2020-07-28 12:40:47 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-23 10:21:51 +00:00
|
|
|
const deviceManifest =
|
|
|
|
await balena.models.config.getDeviceTypeManifestBySlug(deviceType);
|
2022-10-17 14:00:29 +00:00
|
|
|
|
2023-05-12 14:02:32 +00:00
|
|
|
const { validateSecureBootOptionAndWarn } = await import(
|
|
|
|
'../../utils/config'
|
|
|
|
);
|
|
|
|
await validateSecureBootOptionAndWarn(
|
|
|
|
options.secureBoot,
|
|
|
|
deviceType,
|
|
|
|
options.version,
|
|
|
|
);
|
|
|
|
|
2020-07-28 12:40:47 +00:00
|
|
|
// Prompt for values
|
|
|
|
// Pass params as an override: if there is any param with exactly the same name as a
|
|
|
|
// required option, that value is used (and the corresponding question is not asked)
|
|
|
|
const answers = await getCliForm().run(deviceManifest.options, {
|
2021-12-19 20:50:03 +00:00
|
|
|
override: { ...options, app: options.fleet, application: options.fleet },
|
2020-07-28 12:40:47 +00:00
|
|
|
});
|
|
|
|
answers.version = options.version;
|
2021-12-24 15:10:59 +00:00
|
|
|
answers.developmentMode = options.dev;
|
2023-05-12 14:02:32 +00:00
|
|
|
answers.secureBoot = options.secureBoot;
|
2021-07-20 06:42:15 +00:00
|
|
|
answers.provisioningKeyName = options['provisioning-key-name'];
|
2022-05-10 18:02:45 +00:00
|
|
|
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
2020-07-28 12:40:47 +00:00
|
|
|
|
|
|
|
// Generate config
|
|
|
|
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
|
|
|
'../../utils/config'
|
|
|
|
);
|
|
|
|
|
|
|
|
let config;
|
2020-11-05 13:15:55 +00:00
|
|
|
if (device) {
|
2020-07-28 12:40:47 +00:00
|
|
|
config = await generateDeviceConfig(
|
2020-11-05 13:15:55 +00:00
|
|
|
device,
|
2020-07-28 12:40:47 +00:00
|
|
|
options.deviceApiKey || options['generate-device-api-key'] || undefined,
|
|
|
|
answers,
|
|
|
|
);
|
2020-11-05 13:15:55 +00:00
|
|
|
} else if (application) {
|
2020-07-28 12:40:47 +00:00
|
|
|
answers.deviceType = deviceType;
|
2020-11-05 13:15:55 +00:00
|
|
|
config = await generateApplicationConfig(application, answers);
|
2020-07-28 12:40:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Output
|
|
|
|
if (options.output != null) {
|
|
|
|
const fs = await import('fs');
|
|
|
|
await fs.promises.writeFile(options.output, JSON.stringify(config));
|
|
|
|
}
|
|
|
|
|
|
|
|
const prettyjson = await import('prettyjson');
|
|
|
|
console.log(prettyjson.render(config));
|
|
|
|
}
|
|
|
|
|
|
|
|
protected readonly missingDeviceOrAppMessage = stripIndent`
|
2021-07-15 13:41:38 +00:00
|
|
|
Either a device or a fleet must be specified.
|
2020-07-28 12:40:47 +00:00
|
|
|
|
|
|
|
See the help page for examples:
|
|
|
|
|
|
|
|
$ balena help config generate
|
|
|
|
`;
|
|
|
|
|
2020-12-10 12:30:17 +00:00
|
|
|
protected readonly deviceTypeNotAllowedMessage =
|
2021-07-15 13:41:38 +00:00
|
|
|
'The --deviceType option can only be used alongside the --fleet option';
|
2020-07-28 12:40:47 +00:00
|
|
|
|
|
|
|
protected async validateOptions(options: FlagsDef) {
|
|
|
|
const { ExpectedError } = await import('../../errors');
|
|
|
|
|
2021-12-19 20:50:03 +00:00
|
|
|
if (options.device == null && options.fleet == null) {
|
2020-07-28 12:40:47 +00:00
|
|
|
throw new ExpectedError(this.missingDeviceOrAppMessage);
|
|
|
|
}
|
|
|
|
|
2021-12-19 20:50:03 +00:00
|
|
|
if (!options.fleet && options.deviceType) {
|
2020-07-28 12:40:47 +00:00
|
|
|
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
|
|
|
}
|
2021-12-24 15:10:59 +00:00
|
|
|
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
|
|
|
await validateDevOptionAndWarn(options.dev, options.version);
|
2020-07-28 12:40:47 +00:00
|
|
|
}
|
|
|
|
}
|