/** * @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'; import { applicationIdInfo, appToFleetFlagMsg, warnify, } from '../../utils/messages'; import { isV13 } from '../../utils/version'; import type { PineDeferred } from 'balena-sdk'; interface FlagsDef { version: string; // OS version application?: string; app?: string; // application alias fleet?: string; 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; help: void; } export default class ConfigGenerateCmd extends Command { public static description = stripIndent` Generate a config.json file. Generate a config.json file for a device or fleet. The target balenaOS version must be specified with the --version option. 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. To avoid interactive questions, specify a command line option for each question that would otherwise be asked. ${applicationIdInfo.split('\n').join('\n\t\t')} `; 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', '$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key ', '$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json', '$ balena config generate --fleet MyFleet --version 2.12.7', '$ balena config generate --fleet myorg/myfleet --version 2.12.7', '$ balena config generate --fleet MyFleet --version 2.12.7 --deviceType fincm3', '$ balena config generate --fleet MyFleet --version 2.12.7 --output config.json', '$ balena config generate --fleet MyFleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15', ]; public static usage = 'config generate'; public static flags: flags.Input = { version: flags.string({ description: 'a balenaOS version', required: true, }), ...(isV13() ? {} : { application: { ...cf.application, exclusive: ['app', 'fleet', 'device'], }, app: { ...cf.app, exclusive: ['application', 'fleet', 'device'] }, appUpdatePollInterval: flags.string({ description: 'DEPRECATED alias for --updatePollInterval', }), }), fleet: { ...cf.fleet, exclusive: ['application', 'app', 'device'] }, device: { ...cf.device, exclusive: ['application', 'app', 'fleet'] }, deviceApiKey: flags.string({ description: 'custom device key - note that this is only supported on balenaOS 2.0.3+', char: 'k', }), deviceType: flags.string({ description: "device type slug (run 'balena devices supported' for possible values)", }), '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: 'supervisor cloud polling interval in minutes (e.g. for variable updates)', }), help: cf.help, }; public static authenticated = true; public async run() { const { flags: options } = this.parse(ConfigGenerateCmd); const { getApplication } = await import('../../utils/sdk'); const balena = getBalenaSdk(); await this.validateOptions(options); let resourceDeviceType: string; let application: ApplicationWithDeviceType | null = null; let device: | (DeviceWithDeviceType & { belongs_to__application: PineDeferred }) | null = null; if (options.device != null) { const { tryAsInteger } = await import('../../utils/validation'); const rawDevice = await balena.models.device.get( tryAsInteger(options.device), { $expand: { is_of__device_type: { $select: 'slug' } } }, ); if (!rawDevice.belongs_to__application) { const { ExpectedError } = await import('../../errors'); throw new ExpectedError(stripIndent` Device ${options.device} does not appear to belong to an accessible fleet. Try with a different device, or use '--fleet' instead of '--device'.`); } device = rawDevice as DeviceWithDeviceType & { belongs_to__application: PineDeferred; }; resourceDeviceType = device.is_of__device_type[0].slug; } else { // Disambiguate application (if is a number, it could either be an ID or a numerical name) application = (await getApplication(balena, options.application!, { $expand: { is_for__device_type: { $select: 'slug' }, }, })) as ApplicationWithDeviceType; resourceDeviceType = application.is_for__device_type[0].slug; } const deviceType = options.deviceType || resourceDeviceType; const deviceManifest = await balena.models.device.getManifestBySlug( deviceType, ); // Check compatibility if application and deviceType provided if (options.application && options.deviceType) { const appDeviceManifest = await balena.models.device.getManifestBySlug( resourceDeviceType, ); const helpers = await import('../../utils/helpers'); if ( !helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest) ) { throw new balena.errors.BalenaInvalidDeviceType( `Device type ${options.deviceType} is incompatible with fleet ${options.application}`, ); } } // 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, { override: options, }); answers.version = options.version; // Generate config const { generateDeviceConfig, generateApplicationConfig } = await import( '../../utils/config' ); let config; if (device) { config = await generateDeviceConfig( device, options.deviceApiKey || options['generate-device-api-key'] || undefined, answers, ); } else if (application) { answers.deviceType = deviceType; config = await generateApplicationConfig(application, answers); } // 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` Either a device or a fleet must be specified. See the help page for examples: $ balena help config generate `; protected readonly deviceTypeNotAllowedMessage = 'The --deviceType option can only be used alongside the --fleet option'; protected async validateOptions(options: FlagsDef) { const { ExpectedError } = await import('../../errors'); if ((options.application || options.app) && process.stderr.isTTY) { console.error(warnify(appToFleetFlagMsg)); } options.application ||= options.app || options.fleet; // Prefer options.application over options.app delete options.app; if (options.device == null && options.application == null) { throw new ExpectedError(this.missingDeviceOrAppMessage); } if (!options.application && options.deviceType) { throw new ExpectedError(this.deviceTypeNotAllowedMessage); } } }