balena-cli
Version:
The official balena Command Line Interface
287 lines (280 loc) • 12.7 kB
JavaScript
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
;