diff --git a/automation/capitanodoc/capitanodoc.ts b/automation/capitanodoc/capitanodoc.ts index f5431a03..cee0d606 100644 --- a/automation/capitanodoc/capitanodoc.ts +++ b/automation/capitanodoc/capitanodoc.ts @@ -122,6 +122,7 @@ const capitanoDoc = { 'build/actions-oclif/os/configure.js', 'build/actions-oclif/os/versions.js', 'build/actions-oclif/os/download.js', + 'build/actions-oclif/os/initialize.js', ], }, { diff --git a/doc/cli.markdown b/doc/cli.markdown index 5550a004..8f7dbc05 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -521,7 +521,9 @@ or 'menu' (will show the interactive menu) #### -d, --drive DRIVE -the drive to write the image to, eg. `/dev/sdb` or `/dev/mmcblk0`. Careful with this as you can erase your hard drive. Check `balena util available-drives` for available options. +the drive to write the image to, eg. `/dev/sdb` or `/dev/mmcblk0`. +Careful with this as you can erase your hard drive. +Check `balena util available-drives` for available options. #### --config CONFIG @@ -1704,28 +1706,38 @@ paths to local files to place into the 'system-connections' directory ## os initialize <image> -Use this command to initialize a device with previously configured operating system image. +Initialize an os image for a device with a previously + configured operating system image. + Note: Initializing the device may ask for administrative permissions because we need to access the raw devices directly. Examples: - $ balena os initialize ../path/rpi.img --type 'raspberry-pi' + $ balena os initialize ../path/rpi.img --type raspberry-pi + +### Arguments + +#### IMAGE + +path to OS image ### Options -#### --yes, -y - -confirm non interactively - -#### --type, -t <type> +#### -t, --type TYPE device type (Check available types with `balena devices supported`) -#### --drive, -d <drive> +#### -d, --drive DRIVE -the drive to write the image to, like `/dev/sdb` or `/dev/mmcblk0`. Careful with this as you can erase your hard drive. Check `balena util available-drives` for available options. +the drive to write the image to, eg. `/dev/sdb` or `/dev/mmcblk0`. +Careful with this as you can erase your hard drive. +Check `balena util available-drives` for available options. + +#### -y, --yes + +answer "yes" to all questions (non interactive use) # Config @@ -2217,7 +2229,9 @@ path to OS image #### -d, --drive DRIVE -drive to flash +the drive to write the image to, eg. `/dev/sdb` or `/dev/mmcblk0`. +Careful with this as you can erase your hard drive. +Check `balena util available-drives` for available options. #### -y, --yes diff --git a/lib/actions-oclif/device/init.ts b/lib/actions-oclif/device/init.ts index 2f95a438..5cd8586b 100644 --- a/lib/actions-oclif/device/init.ts +++ b/lib/actions-oclif/device/init.ts @@ -66,14 +66,7 @@ export default class DeviceInitCmd extends Command { or 'menu' (will show the interactive menu) `, }), - drive: flags.string({ - char: 'd', - description: stripIndent` - the drive to write the image to, eg. \`/dev/sdb\` or \`/dev/mmcblk0\`. \ - Careful with this as you can erase your hard drive. \ - Check \`balena util available-drives\` for available options. - `, - }), + drive: cf.drive, config: flags.string({ description: 'path to the config JSON file, see `balena os build-config`', }), diff --git a/lib/actions-oclif/local/flash.ts b/lib/actions-oclif/local/flash.ts index 8ac9da90..32aff23f 100644 --- a/lib/actions-oclif/local/flash.ts +++ b/lib/actions-oclif/local/flash.ts @@ -65,10 +65,7 @@ export default class LocalFlashCmd extends Command { public static usage = 'local flash '; public static flags: flags.Input = { - drive: flags.string({ - description: 'drive to flash', - char: 'd', - }), + drive: cf.drive, yes: cf.yes, help: cf.help, }; diff --git a/lib/actions-oclif/os/initialize.ts b/lib/actions-oclif/os/initialize.ts new file mode 100644 index 00000000..22468616 --- /dev/null +++ b/lib/actions-oclif/os/initialize.ts @@ -0,0 +1,135 @@ +/** + * @license + * Copyright 2016-2020 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 { flags } from '@oclif/command'; +import Command from '../../command'; +import * as cf from '../../utils/common-flags'; +import { getCliForm, stripIndent } from '../../utils/lazy'; + +interface FlagsDef { + type: string; + drive?: string; + yes: boolean; + help: void; +} + +interface ArgsDef { + image: string; +} + +const INIT_WARNING_MESSAGE = ` + +Note: Initializing the device may ask for administrative permissions +because we need to access the raw devices directly.\ +`; + +export default class OsInitializeCmd extends Command { + public static description = stripIndent` + Initialize an os image for a device. + + Initialize an os image for a device with a previously + configured operating system image. + ${INIT_WARNING_MESSAGE} + `; + + public static examples = [ + '$ balena os initialize ../path/rpi.img --type raspberry-pi', + ]; + + public static args = [ + { + name: 'image', + description: 'path to OS image', + required: true, + }, + ]; + + public static usage = 'os initialize '; + + public static flags: flags.Input = { + type: flags.string({ + description: + 'device type (Check available types with `balena devices supported`)', + char: 't', + required: true, + }), + drive: cf.drive, + yes: cf.yes, + help: cf.help, + }; + + public static authenticated = true; + + public async run() { + const { args: params, flags: options } = this.parse( + OsInitializeCmd, + ); + + const { promisify } = await import('util'); + const umountAsync = promisify((await import('umount')).umount); + const { getManifest, sudo } = await import('../../utils/helpers'); + + console.info(`Initializing device ${INIT_WARNING_MESSAGE}`); + + const manifest = await getManifest(params.image, options.type); + + const answers = await getCliForm().run(manifest.initialization?.options, { + override: { + drive: options.drive, + }, + }); + + if (answers.drive != null) { + const { confirm } = await import('../../utils/patterns'); + await confirm( + options.yes, + `This will erase ${answers.drive}. Are you sure?`, + `Going to erase ${answers.drive}.`, + true, + ); + await umountAsync(answers.drive); + } + + await sudo([ + 'internal', + 'osinit', + params.image, + options.type, + JSON.stringify(answers), + ]); + + if (answers.drive != null) { + // TODO: balena local makes use of ejectAsync, see below + // DO we need this / should we do that here? + + // getDrive = (drive) -> + // driveListAsync().then (drives) -> + // selectedDrive = _.find(drives, device: drive) + + // if not selectedDrive? + // throw new Error("Drive not found: #{drive}") + + // return selectedDrive + // if (os.platform() is 'win32') and selectedDrive.mountpoint? + // ejectAsync = Promise.promisify(require('removedrive').eject) + // return ejectAsync(selectedDrive.mountpoint) + + await umountAsync(answers.drive); + console.info(`You can safely remove ${answers.drive} now`); + } + } +} diff --git a/lib/actions/os.js b/lib/actions/os.js index 69929651..adfd4806 100644 --- a/lib/actions/os.js +++ b/lib/actions/os.js @@ -89,101 +89,3 @@ Example: ); }, }; - -const INIT_WARNING_MESSAGE = `\ -Note: Initializing the device may ask for administrative permissions -because we need to access the raw devices directly.\ -`; - -export const initialize = { - signature: 'os initialize ', - description: 'initialize an os image', - help: `\ -Use this command to initialize a device with previously configured operating system image. - -${INIT_WARNING_MESSAGE} - -Examples: - - $ balena os initialize ../path/rpi.img --type 'raspberry-pi'\ -`, - permission: 'user', - options: [ - commandOptions.yes, - { - signature: 'type', - description: - 'device type (Check available types with `balena devices supported`)', - parameter: 'type', - alias: 't', - required: 'You have to specify a device type', - }, - commandOptions.drive, - ], - action(params, options) { - const Bluebird = require('bluebird'); - const umountAsync = Bluebird.promisify(require('umount').umount); - const patterns = require('../utils/patterns'); - const helpers = require('../utils/helpers'); - - console.info(`\ -Initializing device - -${INIT_WARNING_MESSAGE}\ -`); - return Bluebird.resolve(helpers.getManifest(params.image, options.type)) - .then((manifest) => - getCliForm().run(manifest.initialization?.options, { - override: { - drive: options.drive, - }, - }), - ) - .tap(function (answers) { - if (answers.drive == null) { - return; - } - return patterns - .confirm( - options.yes, - `This will erase ${answers.drive}. Are you sure?`, - `Going to erase ${answers.drive}.`, - true, - ) - .then(() => umountAsync(answers.drive)); - }) - .tap((answers) => - helpers.sudo([ - 'internal', - 'osinit', - params.image, - options.type, - JSON.stringify(answers), - ]), - ) - .then(function (answers) { - if (answers.drive == null) { - return; - } - - // TODO: balena local makes use of ejectAsync, see below - // DO we need this / should we do that here? - - // getDrive = (drive) -> - // driveListAsync().then (drives) -> - // selectedDrive = _.find(drives, device: drive) - - // if not selectedDrive? - // throw new Error("Drive not found: #{drive}") - - // return selectedDrive - // if (os.platform() is 'win32') and selectedDrive.mountpoint? - // ejectAsync = Promise.promisify(require('removedrive').eject) - // return ejectAsync(selectedDrive.mountpoint) - - return umountAsync(answers.drive).tap(() => { - console.info(`You can safely remove ${answers.drive} now`); - }); - }); - }, -}; diff --git a/lib/app-capitano.ts b/lib/app-capitano.ts index b7c7a9db..b361a7fd 100644 --- a/lib/app-capitano.ts +++ b/lib/app-capitano.ts @@ -48,7 +48,6 @@ capitano.command(actions.help.help); // ---------- OS Module ---------- capitano.command(actions.os.buildConfig); -capitano.command(actions.os.initialize); // ---------- Config Module ---------- capitano.command(actions.config.read); diff --git a/lib/preparser.ts b/lib/preparser.ts index 0907ee66..56571db9 100644 --- a/lib/preparser.ts +++ b/lib/preparser.ts @@ -182,6 +182,7 @@ export const convertedCommands = [ 'os:configure', 'os:versions', 'os:download', + 'os:initialize', 'scan', 'settings', 'ssh', diff --git a/lib/utils/common-flags.ts b/lib/utils/common-flags.ts index bac38cee..3b226596 100644 --- a/lib/utils/common-flags.ts +++ b/lib/utils/common-flags.ts @@ -18,6 +18,7 @@ import { flags } from '@oclif/command'; import type { IBooleanFlag } from '@oclif/parser/lib/flags'; +import { stripIndent } from './lazy'; export const application = flags.string({ char: 'a', @@ -65,3 +66,12 @@ export const force: IBooleanFlag = flags.boolean({ char: 'f', description: 'force action if the update lock is set', }); + +export const drive = flags.string({ + char: 'd', + description: stripIndent` + the drive to write the image to, eg. \`/dev/sdb\` or \`/dev/mmcblk0\`. + Careful with this as you can erase your hard drive. + Check \`balena util available-drives\` for available options. + `, +}); diff --git a/lib/utils/patterns.ts b/lib/utils/patterns.ts index eabb0fac..627842c6 100644 --- a/lib/utils/patterns.ts +++ b/lib/utils/patterns.ts @@ -130,6 +130,15 @@ export function selectDeviceType() { }); } +/** + * Display interactive confirmation prompt. + * If the user declines, then either an error will be thrown, + * or `exitWithExpectedError` will be called (if exitIfDeclined true). + * @param yesOption - automatically confirm if true + * @param message - message to display with prompt + * @param yesMessage - message to display if automatically confirming + * @param exitIfDeclined - exitWithExpectedError when decline if true + */ export async function confirm( yesOption: boolean, message: string, diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts index 34961574..0cc71ea3 100644 --- a/tests/commands/help.spec.ts +++ b/tests/commands/help.spec.ts @@ -82,7 +82,7 @@ Additional commands: os build-config build the OS config and save it to the JSON file os configure configure a previously downloaded balenaOS image os download download an unconfigured OS image - os initialize initialize an os image + os initialize initialize an os image for a device os versions show available balenaOS versions for the given device type settings print current settings tag rm remove a tag from an application, device or release diff --git a/typings/umount/index.d.ts b/typings/umount/index.d.ts index 1cb5f1ac..58f74d47 100644 --- a/typings/umount/index.d.ts +++ b/typings/umount/index.d.ts @@ -18,10 +18,10 @@ declare module 'umount' { export const umount: ( device: string, - callback: (err?: any, stdout?: any, stderr?: any) => void, + callback: (err?: Error, stdout?: any, stderr?: any) => void, ) => void; export const isMounted: ( device: string, - callback: (err?: any, isMounted?: boolean) => void, + callback: (err?: Error, isMounted?: boolean) => void, ) => void; }