mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-04-13 14:12:57 +00:00
Fix 'os configure --config', and migrate it to oclif + TypeScript
Also add more non-interactive configuration options: --config-network, --config-wifi-*, --config-app-update-poll-interval Change-type: minor Signed-off-by: Paulo Castro <paulo@balena.io>
This commit is contained in:
parent
a25a52c21b
commit
d94a74dfee
automation/capitanodoc
doc
lib
tests/commands
@ -85,7 +85,7 @@ const capitanoDoc = {
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: ['build/actions/os.js'],
|
||||
files: ['build/actions/os.js', 'build/actions-oclif/os/configure.js'],
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
|
@ -118,6 +118,13 @@ function renderToc(categories: Category[]): string[] {
|
||||
|
||||
const manualCategorySorting: { [category: string]: string[] } = {
|
||||
'Environment Variables': ['envs', 'env rm', 'env add', 'env rename'],
|
||||
OS: [
|
||||
'os versions',
|
||||
'os download',
|
||||
'os build config',
|
||||
'os configure',
|
||||
'os initialize',
|
||||
],
|
||||
};
|
||||
|
||||
function sortCommands(doc: Document): void {
|
||||
|
@ -1132,23 +1132,22 @@ show advanced configuration options
|
||||
|
||||
the path to the output JSON file
|
||||
|
||||
## os configure <image>
|
||||
## os configure IMAGE
|
||||
|
||||
Use this command to configure a previously downloaded operating system image for
|
||||
the specific device or for an application generally.
|
||||
Configure a previously downloaded balenaOS image for a specific device type or
|
||||
balena application.
|
||||
|
||||
This command will try to automatically determine the operating system version in order
|
||||
to correctly configure the image. It may fail to do so however, in which case you'll
|
||||
have to call this command again with the exact version number of the targeted image.
|
||||
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).
|
||||
|
||||
Note that device api keys are only supported on balenaOS 2.0.3+.
|
||||
The --device-type option may be used to override the application's default
|
||||
device type, in case of an application with mixed device types.
|
||||
|
||||
This command still supports the *deprecated* format where the UUID and optionally device key
|
||||
are passed directly on the command line, but the recommended way is to pass either an --app or
|
||||
--device argument. The deprecated format will be removed in a future release.
|
||||
|
||||
In case that you want to configure an image for an application with mixed device types,
|
||||
you can pass the --device-type argument along with --app to specify the target device type.
|
||||
The --device-api-key option is deprecated and will be removed in a future release.
|
||||
A suitable key is automatically generated or fetched if this option is omitted.
|
||||
|
||||
Examples:
|
||||
|
||||
@ -1157,36 +1156,63 @@ Examples:
|
||||
$ balena os configure ../path/rpi3.img --app MyApp
|
||||
$ balena os configure ../path/rpi3.img --app MyApp --version 2.12.7
|
||||
$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3
|
||||
$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3 --config myWifiConfig.json
|
||||
|
||||
### Arguments
|
||||
|
||||
#### IMAGE
|
||||
|
||||
path to a balenaOS image file, e.g. "rpi3.img"
|
||||
|
||||
### Options
|
||||
|
||||
#### --advanced, -v
|
||||
#### -v, --advanced
|
||||
|
||||
show advanced configuration options
|
||||
ask advanced configuration questions (when in interactive mode)
|
||||
|
||||
#### --application, -a, --app <application>
|
||||
#### --app APP
|
||||
|
||||
same as '--application'
|
||||
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
application name
|
||||
|
||||
#### --device, -d <device>
|
||||
#### --config CONFIG
|
||||
|
||||
device uuid
|
||||
path to a pre-generated config.json file to be injected in the OS image
|
||||
|
||||
#### --deviceApiKey, -k <device-api-key>
|
||||
#### --config-app-update-poll-interval CONFIG-APP-UPDATE-POLL-INTERVAL
|
||||
|
||||
custom device key - note that this is only supported on balenaOS 2.0.3+
|
||||
interval (in minutes) for the on-device balena supervisor periodic app update check
|
||||
|
||||
#### --deviceType <device-type>
|
||||
#### --config-network CONFIG-NETWORK
|
||||
|
||||
device type slug
|
||||
device network type (non-interactive configuration)
|
||||
|
||||
#### --version <version>
|
||||
#### --config-wifi-key CONFIG-WIFI-KEY
|
||||
|
||||
a balenaOS version
|
||||
WiFi key (password) (non-interactive configuration)
|
||||
|
||||
#### --config <config>
|
||||
#### --config-wifi-ssid CONFIG-WIFI-SSID
|
||||
|
||||
path to the config JSON file, see `balena os build-config`
|
||||
WiFi SSID (network name) (non-interactive configuration)
|
||||
|
||||
#### -d, --device DEVICE
|
||||
|
||||
device UUID
|
||||
|
||||
#### -k, --device-api-key DEVICE-API-KEY
|
||||
|
||||
custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)
|
||||
|
||||
#### --device-type DEVICE-TYPE
|
||||
|
||||
device type slug (e.g. "raspberrypi3") to override the application device type
|
||||
|
||||
#### --version VERSION
|
||||
|
||||
balenaOS version, for example "2.32.0" or "2.44.0+rev1"
|
||||
|
||||
## os initialize <image>
|
||||
|
||||
|
431
lib/actions-oclif/os/configure.ts
Normal file
431
lib/actions-oclif/os/configure.ts
Normal file
@ -0,0 +1,431 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 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 { Command, flags } from '@oclif/command';
|
||||
import BalenaSdk = require('balena-sdk');
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
advanced?: boolean;
|
||||
app?: string;
|
||||
application?: string;
|
||||
config?: string;
|
||||
'config-app-update-poll-interval'?: number;
|
||||
'config-network'?: string;
|
||||
'config-wifi-key'?: string;
|
||||
'config-wifi-ssid'?: string;
|
||||
device?: string; // device UUID
|
||||
'device-api-key'?: string;
|
||||
'device-type'?: string;
|
||||
help?: void;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface DeferredDevice extends BalenaSdk.Device {
|
||||
belongs_to__application: BalenaSdk.PineDeferred;
|
||||
}
|
||||
|
||||
interface Answers {
|
||||
appUpdatePollInterval: number; // in minutes
|
||||
deviceType: string; // e.g. "raspberrypi3"
|
||||
network: 'ethernet' | 'wifi';
|
||||
version: string; // e.g. "2.32.0+rev1"
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
}
|
||||
|
||||
const deviceApiKeyDeprecationMsg = stripIndent`
|
||||
The --device-api-key option is deprecated and will be removed in a future release.
|
||||
A suitable key is automatically generated or fetched if this option is omitted.`;
|
||||
|
||||
export default class OsConfigureCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Configure a previously downloaded balenaOS image.
|
||||
|
||||
Configure a previously downloaded balenaOS image for a specific device type or
|
||||
balena application.
|
||||
|
||||
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 may be used to override the application's default
|
||||
device type, in case of an application with mixed device types.
|
||||
|
||||
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena os configure ../path/rpi3.img --device 7cf02a6',
|
||||
'$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>',
|
||||
'$ balena os configure ../path/rpi3.img --app MyApp',
|
||||
'$ balena os configure ../path/rpi3.img --app MyApp --version 2.12.7',
|
||||
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3',
|
||||
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3 --config myWifiConfig.json',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'image',
|
||||
required: true,
|
||||
description: 'path to a balenaOS image file, e.g. "rpi3.img"',
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'os configure' to avoid oclif's 'os:configure' topic syntax
|
||||
public static usage =
|
||||
'os configure ' +
|
||||
new CommandHelp({ args: OsConfigureCmd.args }).defaultUsage();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
advanced: flags.boolean({
|
||||
char: 'v',
|
||||
description:
|
||||
'ask advanced configuration questions (when in interactive mode)',
|
||||
}),
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device'],
|
||||
}),
|
||||
application: _.assign({ exclusive: ['app', 'device'] }, cf.application),
|
||||
config: flags.string({
|
||||
description:
|
||||
'path to a pre-generated config.json file to be injected in the OS image',
|
||||
}),
|
||||
'config-app-update-poll-interval': flags.integer({
|
||||
description:
|
||||
'interval (in minutes) for the on-device balena supervisor periodic app update check',
|
||||
}),
|
||||
'config-network': flags.string({
|
||||
description: 'device network type (non-interactive configuration)',
|
||||
options: ['ethernet', 'wifi'],
|
||||
}),
|
||||
'config-wifi-key': flags.string({
|
||||
description: 'WiFi key (password) (non-interactive configuration)',
|
||||
}),
|
||||
'config-wifi-ssid': flags.string({
|
||||
description: 'WiFi SSID (network name) (non-interactive configuration)',
|
||||
}),
|
||||
device: _.assign({ exclusive: ['app', 'application'] }, cf.device),
|
||||
'device-api-key': flags.string({
|
||||
char: 'k',
|
||||
description:
|
||||
'custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)',
|
||||
}),
|
||||
'device-type': flags.string({
|
||||
description:
|
||||
'device type slug (e.g. "raspberrypi3") to override the application device type',
|
||||
}),
|
||||
help: cf.help,
|
||||
version: flags.string({
|
||||
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
OsConfigureCmd,
|
||||
);
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
options.app = undefined;
|
||||
|
||||
await validateOptions(options);
|
||||
|
||||
const devInit = await import('balena-device-init');
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const fs = await import('mz/fs');
|
||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||
'../../utils/config'
|
||||
);
|
||||
const helpers = await import('../../utils/helpers');
|
||||
let app: BalenaSdk.Application | undefined;
|
||||
let device: BalenaSdk.Device | undefined;
|
||||
let deviceTypeSlug: string;
|
||||
|
||||
if (options.device) {
|
||||
device = await balena.models['device'].get(options.device);
|
||||
deviceTypeSlug = device.device_type;
|
||||
} else {
|
||||
app = await balena.models['application'].get(options.application!);
|
||||
await checkDeviceTypeCompatibility(balena, options, app);
|
||||
deviceTypeSlug = options['device-type'] || app.device_type;
|
||||
}
|
||||
|
||||
const deviceTypeManifest = await helpers.getManifest(
|
||||
params.image,
|
||||
deviceTypeSlug,
|
||||
);
|
||||
|
||||
let configJson: import('../../utils/config').ImgConfig | undefined;
|
||||
if (options.config) {
|
||||
const rawConfig = await fs.readFile(options.config, 'utf8');
|
||||
configJson = JSON.parse(rawConfig);
|
||||
}
|
||||
|
||||
const answers: Answers = await askQuestionsForDeviceType(
|
||||
deviceTypeManifest,
|
||||
options,
|
||||
configJson,
|
||||
);
|
||||
if (options.application) {
|
||||
answers.deviceType = deviceTypeSlug;
|
||||
}
|
||||
answers.version =
|
||||
options.version ||
|
||||
(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit));
|
||||
|
||||
if (_.isEmpty(configJson)) {
|
||||
if (device) {
|
||||
configJson = await generateDeviceConfig(
|
||||
device as DeferredDevice,
|
||||
options['device-api-key'],
|
||||
answers,
|
||||
);
|
||||
} else {
|
||||
configJson = await generateApplicationConfig(app!, answers);
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Configuring operating system image');
|
||||
|
||||
await helpers.osProgressHandler(
|
||||
await devInit.configure(
|
||||
params.image,
|
||||
deviceTypeManifest,
|
||||
configJson || {},
|
||||
answers,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateOptions(options: FlagsDef) {
|
||||
if (process.platform === 'win32') {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Unsupported platform error: the 'balena os configure' command currently requires
|
||||
the Windows Subsystem for Linux in order to run on Windows. It was tested with
|
||||
the Ubuntu 18.04 distribution from the Microsoft Store. With WSL, a balena CLI
|
||||
release for Linux (rather than Windows) should be installed: for example, the
|
||||
standalone zip package for Linux. (It is possible to have both a Windows CLI
|
||||
release and a Linux CLI release installed simultaneously.) For more information
|
||||
on WSL and the balena CLI installation options, please check:
|
||||
- https://docs.microsoft.com/en-us/windows/wsl/about
|
||||
- https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
||||
`);
|
||||
}
|
||||
// The 'device' and 'application' options are declared "exclusive" in the oclif
|
||||
// flag definitions above, so oclif will enforce that they are not both used together.
|
||||
if (!options.device && !options.application) {
|
||||
throw new ExpectedError(
|
||||
"Either the '--device' or the '--application' option must be provided",
|
||||
);
|
||||
}
|
||||
if (!options.application && options['device-type']) {
|
||||
throw new ExpectedError(
|
||||
"The '--device-type' option can only be used in conjunction with the '--application' option",
|
||||
);
|
||||
}
|
||||
if (options['device-api-key']) {
|
||||
console.error(stripIndent`
|
||||
-------------------------------------------------------------------------------------------
|
||||
Warning: ${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t\t')}
|
||||
-------------------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
const { checkLoggedIn } = await import('../../utils/patterns');
|
||||
await checkLoggedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around balena-device-init.getImageOsVersion(). Throws ExpectedError
|
||||
* if the OS image could not be read or the OS version could not be extracted
|
||||
* from it.
|
||||
* @param imagePath Local filesystem path to a balenaOS image file
|
||||
* @param deviceTypeManifest Device type manifest object
|
||||
*/
|
||||
async function getOsVersionFromImage(
|
||||
imagePath: string,
|
||||
deviceTypeManifest: BalenaSdk.DeviceType,
|
||||
devInit: typeof import('balena-device-init'),
|
||||
): Promise<string> {
|
||||
const osVersion = await devInit.getImageOsVersion(
|
||||
imagePath,
|
||||
deviceTypeManifest,
|
||||
);
|
||||
if (!osVersion) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Could not read OS version from the image. Please specify the balenaOS
|
||||
version manually with the --version command-line option.`);
|
||||
}
|
||||
return osVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that options['device-type'], e.g. 'raspberrypi3', is compatible with
|
||||
* app.device_type, e.g. 'raspberry-pi2'. Throws ExpectedError if they are not
|
||||
* compatible.
|
||||
* @param sdk Balena Node SDK instance
|
||||
* @param options oclif command-line options object
|
||||
* @param app Balena SDK Application model object
|
||||
*/
|
||||
async function checkDeviceTypeCompatibility(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
options: FlagsDef,
|
||||
app: BalenaSdk.Application,
|
||||
) {
|
||||
if (options['device-type']) {
|
||||
const [appDeviceType, optionDeviceType] = await Promise.all([
|
||||
sdk.models.device.getManifestBySlug(app.device_type),
|
||||
sdk.models.device.getManifestBySlug(options['device-type']),
|
||||
]);
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
|
||||
throw new ExpectedError(
|
||||
`Device type ${
|
||||
options['device-type']
|
||||
} is incompatible with application ${options.application}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given options or configJson objects (in this order) contain
|
||||
* the answers to some configuration questions, and interactively ask the
|
||||
* user the questions for which answers are missing. Questions such as:
|
||||
*
|
||||
* ? Network Connection (Use arrow keys)
|
||||
* ethernet
|
||||
* ❯ wifi
|
||||
* ? Network Connection wifi
|
||||
* ? Wifi SSID i-ssid
|
||||
* ? Wifi Passphrase [input is hidden]
|
||||
*
|
||||
* The questions are extracted from the given deviceType "manifest".
|
||||
*/
|
||||
async function askQuestionsForDeviceType(
|
||||
deviceType: BalenaSdk.DeviceType,
|
||||
options: FlagsDef,
|
||||
configJson?: import('../../utils/config').ImgConfig,
|
||||
): Promise<Answers> {
|
||||
const form = await import('resin-cli-form');
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const answerSources: any[] = [camelifyConfigOptions(options)];
|
||||
const defaultAnswers: Partial<Answers> = {};
|
||||
const questions: any = deviceType.options;
|
||||
let extraOpts: { override: object } | undefined;
|
||||
|
||||
if (!_.isEmpty(configJson)) {
|
||||
answerSources.push(configJson);
|
||||
}
|
||||
|
||||
if (!options.advanced) {
|
||||
const advancedGroup: any = _.find(questions, {
|
||||
name: 'advanced',
|
||||
isGroup: true,
|
||||
});
|
||||
if (!_.isEmpty(advancedGroup)) {
|
||||
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 form.run(questions, extraOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a deviceType "manifest" containing "options" properties, return an
|
||||
* array of "question names" as in the following example.
|
||||
*
|
||||
* @param deviceType Device type "manifest", for example:
|
||||
* { "slug": "raspberrypi3",
|
||||
* "options": [{
|
||||
* "options": [ {
|
||||
* "name": "network",
|
||||
* "choices": ["ethernet", "wifi"],
|
||||
* ... }, {
|
||||
* "name": "wifiSsid",
|
||||
* "type": "text",
|
||||
* ... }, {
|
||||
* "options": [ {
|
||||
* "name": "appUpdatePollInterval",
|
||||
* "default": 10,
|
||||
* ...
|
||||
* @return Array of question names, for example:
|
||||
* [ 'network', 'wifiSsid', 'wifiKey', 'appUpdatePollInterval' ]
|
||||
*/
|
||||
function getQuestionNames(
|
||||
deviceType: BalenaSdk.DeviceType,
|
||||
): Array<keyof Answers> {
|
||||
const questionNames: string[] = _.chain(deviceType.options)
|
||||
.flatMap(
|
||||
(group: BalenaSdk.DeviceTypeOptions) =>
|
||||
(group.isGroup && group.options) || [],
|
||||
)
|
||||
.map((groupOption: BalenaSdk.DeviceTypeOptionsGroup) => groupOption.name)
|
||||
.filter()
|
||||
.value();
|
||||
return questionNames as Array<keyof Answers>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new object with the key-value pairs from the input object,
|
||||
* renaming keys that start with the 'config-' prefix as follows:
|
||||
* Sample input:
|
||||
* { app: 'foo', 'config-wifi-key': 'mykey', 'config-wifi-ssid': 'myssid' }
|
||||
* Output:
|
||||
* { app: 'foo', wifiKey: 'mykey', wifiSsid: 'myssid' }
|
||||
*/
|
||||
function camelifyConfigOptions(options: FlagsDef): { [key: string]: any } {
|
||||
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;
|
||||
});
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
_ = require('lodash')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
formatVersion = (v, isRecommended) ->
|
||||
result = "v#{v}"
|
||||
@ -202,143 +201,6 @@ exports.buildConfig =
|
||||
writeFileAsync(options.output, JSON.stringify(answers, null, 4))
|
||||
.nodeify(done)
|
||||
|
||||
exports.configure =
|
||||
signature: 'os configure <image>'
|
||||
description: 'configure an os image'
|
||||
help: '''
|
||||
Use this command to configure a previously downloaded operating system image for
|
||||
the specific device or for an application generally.
|
||||
|
||||
This command will try to automatically determine the operating system version in order
|
||||
to correctly configure the image. It may fail to do so however, in which case you'll
|
||||
have to call this command again with the exact version number of the targeted image.
|
||||
|
||||
Note that device api keys are only supported on balenaOS 2.0.3+.
|
||||
|
||||
This command still supports the *deprecated* format where the UUID and optionally device key
|
||||
are passed directly on the command line, but the recommended way is to pass either an --app or
|
||||
--device argument. The deprecated format will be removed in a future release.
|
||||
|
||||
In case that you want to configure an image for an application with mixed device types,
|
||||
you can pass the --device-type argument along with --app to specify the target device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena os configure ../path/rpi3.img --device 7cf02a6
|
||||
$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>
|
||||
$ balena os configure ../path/rpi3.img --app MyApp
|
||||
$ balena os configure ../path/rpi3.img --app MyApp --version 2.12.7
|
||||
$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
commandOptions.advancedConfig
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
commandOptions.optionalDeviceType
|
||||
commandOptions.optionalOsVersion
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `balena os build-config`'
|
||||
parameter: 'config'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
fs = require('fs')
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(fs.readFile)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
init = require('balena-device-init')
|
||||
helpers = require('../utils/helpers')
|
||||
patterns = require('../utils/patterns')
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
|
||||
if _.filter([
|
||||
options.device
|
||||
options.application
|
||||
]).length != 1
|
||||
patterns.exitWithExpectedError '''
|
||||
To configure an image, you must provide exactly one of:
|
||||
|
||||
* A device, with --device <uuid>
|
||||
* An application, with --app <appname>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help os configure
|
||||
'''
|
||||
|
||||
if !options.application and options.deviceType
|
||||
patterns.exitWithExpectedError '''
|
||||
Specifying a different device type is only supported when
|
||||
configuring an image using an application as a parameter:
|
||||
|
||||
* An application, with --app <appname>
|
||||
* A specific device type, with --device-type <deviceTypeSlug>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help os configure
|
||||
'''
|
||||
|
||||
uuid = options.device
|
||||
deviceApiKey = options.deviceApiKey
|
||||
|
||||
console.info('Configuring operating system image')
|
||||
|
||||
configurationResourceType = if uuid then 'device' else 'application'
|
||||
|
||||
balena.models[configurationResourceType].get(uuid || options.application)
|
||||
.then (appOrDevice) ->
|
||||
deviceType = options.deviceType || appOrDevice.device_type
|
||||
manifestPromise = helpers.getManifest(params.image, deviceType)
|
||||
|
||||
if options.application && options.deviceType
|
||||
app = appOrDevice
|
||||
appManifestPromise = balena.models.device.getManifestBySlug(app.device_type)
|
||||
paramManifestPromise = balena.models.device.getManifestBySlug(options.deviceType)
|
||||
manifestPromise = Promise.resolve(manifestPromise).tap ->
|
||||
Promise.join appManifestPromise, paramManifestPromise, (appDeviceType, paramDeviceType) ->
|
||||
if not helpers.areDeviceTypesCompatible(appDeviceType, paramDeviceType)
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
"Device type #{options.deviceType} is incompatible with application #{options.application}"
|
||||
)
|
||||
|
||||
answersPromise = Promise.try ->
|
||||
if options.config
|
||||
return readFileAsync(options.config, 'utf8')
|
||||
.then(JSON.parse)
|
||||
return manifestPromise.then (deviceTypeManifest) ->
|
||||
buildConfigForDeviceType(deviceTypeManifest, options.advanced)
|
||||
|
||||
Promise.join answersPromise, manifestPromise, (answers, manifest) ->
|
||||
answers.version = options.version
|
||||
|
||||
if configurationResourceType == 'application'
|
||||
answers.deviceType = deviceType
|
||||
|
||||
if not answers.version?
|
||||
answers.version = Promise.resolve(helpers.getOsVersion(params.image, manifest)).tap (version) ->
|
||||
if not version?
|
||||
throw new Error(
|
||||
'Could not read OS version from the image. ' +
|
||||
'Please specify the version manually with the ' +
|
||||
'--version argument to this command.'
|
||||
)
|
||||
|
||||
Promise.props(answers).then (answers) ->
|
||||
(if configurationResourceType == 'device'
|
||||
generateDeviceConfig(appOrDevice, deviceApiKey, answers)
|
||||
else
|
||||
generateApplicationConfig(appOrDevice, answers)
|
||||
)
|
||||
.then (config) ->
|
||||
init.configure(params.image, manifest, config, answers)
|
||||
.then(helpers.osProgressHandler)
|
||||
.nodeify(done)
|
||||
|
||||
INIT_WARNING_MESSAGE = '''
|
||||
Note: Initializing the device may ask for administrative permissions
|
||||
because we need to access the raw devices directly.
|
||||
|
@ -93,7 +93,6 @@ capitano.command(actions.tags.remove)
|
||||
capitano.command(actions.os.versions)
|
||||
capitano.command(actions.os.download)
|
||||
capitano.command(actions.os.buildConfig)
|
||||
capitano.command(actions.os.configure)
|
||||
capitano.command(actions.os.initialize)
|
||||
|
||||
# ---------- Config Module ----------
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -132,6 +132,7 @@ export const convertedCommands = [
|
||||
'env:add',
|
||||
'env:rename',
|
||||
'env:rm',
|
||||
'os:configure',
|
||||
'version',
|
||||
];
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -19,7 +19,7 @@ import * as semver from 'resin-semver';
|
||||
|
||||
const balena = BalenaSdk.fromSharedOptions();
|
||||
|
||||
interface ImgConfig {
|
||||
export interface ImgConfig {
|
||||
applicationName: string;
|
||||
applicationId: number;
|
||||
deviceType: string;
|
||||
@ -103,7 +103,7 @@ export function generateDeviceConfig(
|
||||
device: BalenaSdk.Device & {
|
||||
belongs_to__application: BalenaSdk.PineDeferred;
|
||||
},
|
||||
deviceApiKey: string | true | null,
|
||||
deviceApiKey: string | true | undefined,
|
||||
options: { version: string },
|
||||
) {
|
||||
return balena.models.application
|
||||
|
@ -28,7 +28,7 @@ const balena = BalenaSdk.fromSharedOptions();
|
||||
|
||||
export function getGroupDefaults(group: {
|
||||
options: Array<{ name: string; default?: string }>;
|
||||
}): { [name: string]: string | undefined } {
|
||||
}): { [name: string]: string | number | undefined } {
|
||||
return _.chain(group)
|
||||
.get('options')
|
||||
.map(question => [question.name, question.default])
|
||||
@ -122,14 +122,6 @@ export const areDeviceTypesCompatible = (
|
||||
deviceTypeA.arch === deviceTypeB.arch &&
|
||||
!!deviceTypeA.isDependent === !!deviceTypeB.isDependent;
|
||||
|
||||
export async function getOsVersion(
|
||||
image: string,
|
||||
manifest: BalenaSdk.DeviceType,
|
||||
): Promise<string | null> {
|
||||
const init = await import('balena-device-init');
|
||||
return init.getImageOsVersion(image, manifest);
|
||||
}
|
||||
|
||||
export async function osProgressHandler(step: InitializeEmitter) {
|
||||
step.on('stdout', process.stdout.write.bind(process.stdout));
|
||||
step.on('stderr', process.stderr.write.bind(process.stderr));
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -71,7 +71,7 @@ Additional commands:
|
||||
logout logout from balena
|
||||
note <|note> set a device note
|
||||
os build-config <image> <device-type> build the OS config and save it to the JSON file
|
||||
os configure <image> configure an os image
|
||||
os configure <image> configure a previously downloaded balenaOS image
|
||||
os download <type> download an unconfigured os image
|
||||
os initialize <image> initialize an os image
|
||||
os versions <type> show the available balenaOS versions for the given device type
|
||||
|
Loading…
x
Reference in New Issue
Block a user