UNPKG

balena-cli

Version:

The official balena Command Line Interface

287 lines (280 loc) • 12.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@oclif/core"); const util_1 = require("util"); const _ = require("lodash"); const errors_1 = require("../../errors"); const cf = require("../../utils/common-flags"); const lazy_1 = require("../../utils/lazy"); const messages_1 = require("../../utils/messages"); const CONNECTIONS_FOLDER = '/system-connections'; class OsConfigureCmd extends core_1.Command { async run() { const { args: params, flags: options } = await this.parse(OsConfigureCmd); await validateOptions(options); const devInit = await Promise.resolve().then(() => require('balena-device-init')); const { promises: fs } = await Promise.resolve().then(() => require('fs')); const { generateDeviceConfig, generateApplicationConfig } = await Promise.resolve().then(() => require('../../utils/config')); const helpers = await Promise.resolve().then(() => require('../../utils/helpers')); const { getApplication } = await Promise.resolve().then(() => require('../../utils/sdk')); let app; let device; let deviceTypeSlug; const balena = (0, lazy_1.getBalenaSdk)(); if (options.device) { device = (await balena.models.device.get(options.device, { $expand: { is_of__device_type: { $select: 'slug' }, }, })); deviceTypeSlug = device.is_of__device_type[0].slug; } else { app = (await getApplication(balena, options.fleet, { $expand: { is_for__device_type: { $select: 'slug' }, }, })); await checkDeviceTypeCompatibility(options, app); deviceTypeSlug = options['device-type'] || app.is_for__device_type[0].slug; } const deviceTypeManifest = await helpers.getManifest(params.image, deviceTypeSlug); let configJson; if (options.config) { const rawConfig = await fs.readFile(options.config, 'utf8'); configJson = JSON.parse(rawConfig); } const { normalizeOsVersion } = await Promise.resolve().then(() => require('../../utils/normalization')); const osVersion = normalizeOsVersion(options.version || (await getOsVersionFromImage(params.image, deviceTypeManifest, devInit))); const { validateDevOptionAndWarn } = await Promise.resolve().then(() => require('../../utils/config')); await validateDevOptionAndWarn(options.dev, osVersion); const { validateSecureBootOptionAndWarn } = await Promise.resolve().then(() => require('../../utils/config')); await validateSecureBootOptionAndWarn(options.secureBoot, deviceTypeSlug, osVersion); const answers = await askQuestionsForDeviceType(deviceTypeManifest, options, configJson); if (options.fleet) { answers.deviceType = deviceTypeSlug; } answers.version = osVersion; answers.developmentMode = options.dev; answers.secureBoot = options.secureBoot; answers.provisioningKeyName = options['provisioning-key-name']; answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date']; if (_.isEmpty(configJson)) { if (device) { configJson = await generateDeviceConfig(device, undefined, answers); } else { configJson = await generateApplicationConfig(app, answers); } } if (options['initial-device-name'] && options['initial-device-name'] !== '') { configJson.initialDeviceName = options['initial-device-name']; } console.info('Configuring operating system image'); const image = params.image; await helpers.osProgressHandler(await devInit.configure(image, deviceTypeManifest, configJson || {}, answers)); if (options['system-connection']) { const path = await Promise.resolve().then(() => require('path')); const files = await Promise.all(options['system-connection'].map(async (filePath) => { const content = await fs.readFile(filePath, 'utf8'); const name = path.basename(filePath); return { name, content, }; })); const { getBootPartition } = await Promise.resolve().then(() => require('balena-config-json')); const bootPartition = await getBootPartition(params.image); const imagefs = await Promise.resolve().then(() => require('balena-image-fs')); for (const { name, content } of files) { await imagefs.interact(image, bootPartition, async (_fs) => { return await (0, util_1.promisify)(_fs.writeFile)(path.join(CONNECTIONS_FOLDER, name), content); }); console.info(`Copied system-connection file: ${name}`); } } } } OsConfigureCmd.description = (0, lazy_1.stripIndent) ` Configure a previously downloaded balenaOS image. Configure a previously downloaded balenaOS image for a specific device type or fleet. 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 is used to override the fleet's default device type, in case of a fleet with mixed device types. ${messages_1.devModeInfo.split('\n').join('\n\t\t')} ${messages_1.secureBootInfo.split('\n').join('\n\t\t')} The --system-connection (-c) option is 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/ref-settings.html ${messages_1.applicationIdInfo.split('\n').join('\n\t\t')} `; OsConfigureCmd.examples = [ '$ balena os configure ../path/rpi3.img --device 7cf02a6', '$ 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', ]; OsConfigureCmd.args = { image: core_1.Args.string({ required: true, description: 'path to a balenaOS image file, e.g. "rpi3.img"', }), }; OsConfigureCmd.flags = { advanced: core_1.Flags.boolean({ char: 'v', description: 'ask advanced configuration questions (when in interactive mode)', }), fleet: { ...cf.fleet, exclusive: ['device'] }, config: core_1.Flags.string({ description: 'path to a pre-generated config.json file to be injected in the OS image', exclusive: ['provisioning-key-name', 'provisioning-key-expiry-date'], }), 'config-app-update-poll-interval': core_1.Flags.integer({ description: 'supervisor cloud polling interval in minutes (e.g. for variable updates)', }), 'config-network': core_1.Flags.string({ description: 'device network type (non-interactive configuration)', options: ['ethernet', 'wifi'], }), 'config-wifi-key': core_1.Flags.string({ description: 'WiFi key (password) (non-interactive configuration)', }), 'config-wifi-ssid': core_1.Flags.string({ description: 'WiFi SSID (network name) (non-interactive configuration)', }), dev: cf.dev, secureBoot: cf.secureBoot, device: { ...cf.device, exclusive: [ 'fleet', 'provisioning-key-name', 'provisioning-key-expiry-date', ], }, 'device-type': core_1.Flags.string({ description: 'device type slug (e.g. "raspberrypi3") to override the fleet device type', }), 'initial-device-name': core_1.Flags.string({ description: 'This option will set the device name when the device provisions', }), version: core_1.Flags.string({ description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"', }), 'system-connection': core_1.Flags.string({ multiple: true, char: 'c', required: false, description: "paths to local files to place into the 'system-connections' directory", }), 'provisioning-key-name': core_1.Flags.string({ description: 'custom key name assigned to generated provisioning api key', exclusive: ['config', 'device'], }), 'provisioning-key-expiry-date': core_1.Flags.string({ description: 'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)', exclusive: ['config', 'device'], }), }; OsConfigureCmd.authenticated = true; exports.default = OsConfigureCmd; async function validateOptions(options) { if (!options.device && !options.fleet) { throw new errors_1.ExpectedError("Either the '--device' or the '--fleet' option must be provided"); } if (!options.fleet && options['device-type']) { throw new errors_1.ExpectedError("The '--device-type' option can only be used in conjunction with the '--fleet' option"); } const { checkLoggedIn } = await Promise.resolve().then(() => require('../../utils/patterns')); await checkLoggedIn(); } async function getOsVersionFromImage(imagePath, deviceTypeManifest, devInit) { const osVersion = await devInit.getImageOsVersion(imagePath, deviceTypeManifest); if (!osVersion) { throw new errors_1.ExpectedError((0, lazy_1.stripIndent) ` Could not read OS version from the image. Please specify the balenaOS version manually with the --version command-line option.`); } return osVersion; } async function checkDeviceTypeCompatibility(options, app) { if (options['device-type']) { const helpers = await Promise.resolve().then(() => require('../../utils/helpers')); if (!(await helpers.areDeviceTypesCompatible(app.is_for__device_type[0].slug, options['device-type']))) { throw new errors_1.ExpectedError(`Device type ${options['device-type']} is incompatible with fleet ${options.fleet}`); } } } async function askQuestionsForDeviceType(deviceType, options, configJson) { const answerSources = [ { ...camelifyConfigOptions(options), app: options.fleet, application: options.fleet, }, ]; const defaultAnswers = {}; const questions = deviceType.options; let extraOpts; if (!_.isEmpty(configJson)) { answerSources.push(configJson); } if (!options.advanced) { const advancedGroup = _.find(questions, { name: 'advanced', isGroup: true, }); if (!_.isEmpty(advancedGroup)) { const helpers = await Promise.resolve().then(() => require('../../utils/helpers')); 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 (0, lazy_1.getCliForm)().run(questions, extraOpts); } function getQuestionNames(deviceType) { const questionNames = _.chain(deviceType.options) .flatMap((group) => (group.isGroup && group.options) || []) .map((groupOption) => groupOption.name) .filter() .value(); return questionNames; } function camelifyConfigOptions(options) { return _.mapKeys(options, (_value, key) => { if (key.startsWith('config-')) { return key .substring('config-'.length) .replace(/-[a-z]/g, (match) => match.substring(1).toUpperCase()); } return key; }); } //# sourceMappingURL=configure.js.map