UNPKG

balena-cli

Version:

The official balena Command Line Interface

302 lines (295 loc) • 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@oclif/core"); 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() { var _a; const { args: params, flags: options } = await this.parse(OsConfigureCmd); await validateArgsAndOptions(params, options); const devInit = await Promise.resolve().then(() => require('balena-device-init')); const { generateDeviceConfig, generateApplicationConfig, readAndValidateConfigJson, } = 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 = options['device-type']; let fleetSlugOrId = options.fleet; let secureBoot = options.secureBoot; let developmentMode = options.dev; const balena = (0, lazy_1.getBalenaSdk)(); let configJson; if (options.config != null) { configJson = await readAndValidateConfigJson(options.config); fleetSlugOrId = configJson.applicationId; deviceTypeSlug = configJson.deviceType; secureBoot = ((_a = configJson.installer) === null || _a === void 0 ? void 0 : _a.secureboot) === true; developmentMode = configJson.developmentMode === true; } 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 if (fleetSlugOrId != null) { app = await getApplication(balena, fleetSlugOrId, { $select: 'slug', $expand: { is_for__device_type: { $select: 'slug' }, }, }); await checkDeviceTypeCompatibility(deviceTypeSlug, app); deviceTypeSlug !== null && deviceTypeSlug !== void 0 ? deviceTypeSlug : (deviceTypeSlug = app.is_for__device_type[0].slug); } else { throw new Error('Unreachable: neither a device nor a fleet were specified or resolved by the config json'); } const deviceTypeManifest = await helpers.getManifest(params.image, deviceTypeSlug); const { normalizeOsVersion } = await Promise.resolve().then(() => require('../../utils/normalization')); const osVersion = normalizeOsVersion(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit)); const { validateDevOptionAndWarn } = await Promise.resolve().then(() => require('../../utils/config')); await validateDevOptionAndWarn(developmentMode, osVersion); const { validateSecureBootOptionAndWarn } = await Promise.resolve().then(() => require('../../utils/config')); await validateSecureBootOptionAndWarn(secureBoot, deviceTypeSlug, osVersion); const _ = await Promise.resolve().then(() => require('lodash')); const baseAnswers = configJson == null ? await askQuestionsForDeviceType(deviceTypeManifest, options) : { appUpdatePollInterval: configJson.appUpdatePollInterval, network: !configJson.wifiSsid && !configJson.wifiKey ? 'ethernet' : 'wifi', ..._.pick(configJson, ['wifiSsid', 'wifiKey']), }; const answers = { ...baseAnswers, deviceType: deviceTypeSlug, version: osVersion, developmentMode: developmentMode, secureBoot: secureBoot, }; if (configJson == null) { answers.provisioningKeyName = options['provisioning-key-name']; answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date']; if (device != null) { configJson = await generateDeviceConfig(device, undefined, answers); } else { configJson = await generateApplicationConfig(app, answers); } if (options['initial-device-name'] != null && 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 fs = await Promise.resolve().then(() => require('fs/promises')); 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) => { await _fs.promises.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 -f myorg/myfleet --device-type raspberrypi3', '$ balena os configure ../path/rpi3.img --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 = (() => { const inlineConfigFlags = { advanced: core_1.Flags.boolean({ char: 'v', description: 'ask advanced configuration questions (when in interactive mode)', }), '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-type': core_1.Flags.string({ description: 'device type slug (e.g. "raspberrypi3") to override the fleet device type', dependsOn: ['fleet'], }), 'initial-device-name': core_1.Flags.string({ description: 'This option will set the device name when the device provisions', }), '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'], }), }; return { fleet: { ...cf.fleet, exclusive: ['device', 'config'] }, device: { ...cf.device, exclusive: [ 'fleet', 'device-type', 'config', 'provisioning-key-name', 'provisioning-key-expiry-date', ], }, config: core_1.Flags.string({ description: 'path to a pre-generated config.json file to be injected in the OS image', exclusive: ['fleet', 'device', ...Object.keys(inlineConfigFlags)], }), ...inlineConfigFlags, 'system-connection': core_1.Flags.string({ multiple: true, char: 'c', required: false, description: "paths to local files to place into the 'system-connections' directory", }), }; })(); OsConfigureCmd.authenticated = true; exports.default = OsConfigureCmd; async function validateArgsAndOptions(args, options) { if (!options.device && !options.fleet && !options.config) { throw new errors_1.ExpectedError("One of the '--device', '--fleet' or '--config' options must be provided"); } const { validateFilePath } = await Promise.resolve().then(() => require('../../utils/validation')); await validateFilePath(args.image); if (options.config != null) { await validateFilePath(options.config); } 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('Could not read OS version from the image.'); } return osVersion; } async function checkDeviceTypeCompatibility(deviceType, app) { if (deviceType) { const helpers = await Promise.resolve().then(() => require('../../utils/helpers')); if (!(await helpers.areDeviceTypesCompatible(app.is_for__device_type[0].slug, deviceType))) { throw new errors_1.ExpectedError(`Device type ${deviceType} is incompatible with fleet ${app.is_for__device_type[0].slug}`); } } } async function askQuestionsForDeviceType(deviceType, options) { var _a; const answerSources = [ { ...camelifyConfigOptions(options), app: options.fleet, application: options.fleet, }, ]; const defaultAnswers = {}; const questions = (_a = deviceType.options) !== null && _a !== void 0 ? _a : []; let extraOpts; if (!options.advanced) { const advancedGroup = questions.find((question) => question.name === 'advanced' && question.isGroup); if (advancedGroup != null && Object.keys(advancedGroup).length > 0) { 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 (defaultAnswers != null && Object.keys(defaultAnswers).length > 0) { extraOpts = { override: defaultAnswers }; } return (await (0, lazy_1.getCliForm)().run(questions, extraOpts)); } function getQuestionNames(deviceType) { var _a, _b; const questionNames = (_b = (_a = deviceType.options) === null || _a === void 0 ? void 0 : _a.flatMap((group) => (group.isGroup && group.options) || []).map((groupOption) => groupOption.name).filter(Boolean)) !== null && _b !== void 0 ? _b : []; return questionNames; } function camelifyConfigOptions(options) { return Object.fromEntries(Object.entries(options).map(([key, value]) => { if (key.startsWith('config-')) { return [ key .substring('config-'.length) .replace(/-[a-z]/g, (match) => match.substring(1).toUpperCase()), value, ]; } return [key, value]; })); } //# sourceMappingURL=configure.js.map