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