mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-23 15:32:22 +00:00
Merge pull request #1052 from balena-io/update-local-flash-action
Update "local flash" and "util available-drives" actions
This commit is contained in:
commit
5cf0f7030d
@ -1,120 +0,0 @@
|
|||||||
###
|
|
||||||
Copyright 2017 Balena
|
|
||||||
|
|
||||||
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.
|
|
||||||
###
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
signature: 'local flash <image>'
|
|
||||||
description: 'Flash an image to a drive'
|
|
||||||
help: '''
|
|
||||||
Use this command to flash a balenaOS image to a drive.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ balena local flash path/to/balenaos.img
|
|
||||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2
|
|
||||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
|
|
||||||
'''
|
|
||||||
options: [
|
|
||||||
signature: 'yes'
|
|
||||||
boolean: true
|
|
||||||
description: 'confirm non-interactively'
|
|
||||||
alias: 'y'
|
|
||||||
,
|
|
||||||
signature: 'drive'
|
|
||||||
parameter: 'drive'
|
|
||||||
description: 'drive'
|
|
||||||
alias: 'd'
|
|
||||||
]
|
|
||||||
root: true
|
|
||||||
action: (params, options, done) ->
|
|
||||||
|
|
||||||
_ = require('lodash')
|
|
||||||
os = require('os')
|
|
||||||
Promise = require('bluebird')
|
|
||||||
umountAsync = Promise.promisify(require('umount').umount)
|
|
||||||
fs = Promise.promisifyAll(require('fs'))
|
|
||||||
driveListAsync = Promise.promisify(require('drivelist').list)
|
|
||||||
chalk = require('chalk')
|
|
||||||
visuals = require('resin-cli-visuals')
|
|
||||||
form = require('resin-cli-form')
|
|
||||||
imageWrite = require('etcher-image-write')
|
|
||||||
|
|
||||||
form.run [
|
|
||||||
{
|
|
||||||
message: 'Select drive'
|
|
||||||
type: 'drive'
|
|
||||||
name: 'drive'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'This will erase the selected drive. Are you sure?'
|
|
||||||
type: 'confirm'
|
|
||||||
name: 'yes'
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
override:
|
|
||||||
drive: options.drive
|
|
||||||
|
|
||||||
# If `options.yes` is `false`, pass `undefined`,
|
|
||||||
# otherwise the question will not be asked because
|
|
||||||
# `false` is a defined value.
|
|
||||||
yes: options.yes || undefined
|
|
||||||
|
|
||||||
# TODO: dedupe with the resin-device-operations
|
|
||||||
.then (answers) ->
|
|
||||||
if answers.yes isnt true
|
|
||||||
console.log(chalk.red.bold('Aborted image flash'))
|
|
||||||
process.exit(0)
|
|
||||||
|
|
||||||
driveListAsync().then (drives) ->
|
|
||||||
selectedDrive = _.find(drives, device: answers.drive)
|
|
||||||
|
|
||||||
if not selectedDrive?
|
|
||||||
throw new Error("Drive not found: #{answers.drive}")
|
|
||||||
|
|
||||||
return selectedDrive
|
|
||||||
.then (selectedDrive) ->
|
|
||||||
progressBars =
|
|
||||||
write: new visuals.Progress('Flashing')
|
|
||||||
check: new visuals.Progress('Validating')
|
|
||||||
|
|
||||||
umountAsync(selectedDrive.device).then ->
|
|
||||||
Promise.props
|
|
||||||
imageSize: fs.statAsync(params.image).get('size'),
|
|
||||||
imageStream: Promise.resolve(fs.createReadStream(params.image))
|
|
||||||
driveFileDescriptor: fs.openAsync(selectedDrive.raw, 'rs+')
|
|
||||||
.then (results) ->
|
|
||||||
imageWrite.write
|
|
||||||
fd: results.driveFileDescriptor
|
|
||||||
device: selectedDrive.raw
|
|
||||||
size: selectedDrive.size
|
|
||||||
,
|
|
||||||
stream: results.imageStream,
|
|
||||||
size: results.imageSize
|
|
||||||
,
|
|
||||||
check: true
|
|
||||||
.then (writer) ->
|
|
||||||
new Promise (resolve, reject) ->
|
|
||||||
writer.on 'progress', (state) ->
|
|
||||||
progressBars[state.type].update(state)
|
|
||||||
writer.on('error', reject)
|
|
||||||
writer.on('done', resolve)
|
|
||||||
.then ->
|
|
||||||
if (os.platform() is 'win32') and selectedDrive.mountpoint?
|
|
||||||
ejectAsync = Promise.promisify(require('removedrive').eject)
|
|
||||||
return ejectAsync(selectedDrive.mountpoint)
|
|
||||||
|
|
||||||
return umountAsync(selectedDrive.device)
|
|
||||||
.asCallback(done)
|
|
121
lib/actions/local/flash.ts
Normal file
121
lib/actions/local/flash.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 Balena
|
||||||
|
|
||||||
|
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 { CommandDefinition } from 'capitano';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { stripIndent } from 'common-tags';
|
||||||
|
import * as sdk from 'etcher-sdk';
|
||||||
|
|
||||||
|
import { DriveList } from '../../utils/visuals/drive-list';
|
||||||
|
|
||||||
|
async function getDrive(options: {
|
||||||
|
drive?: string;
|
||||||
|
}): Promise<sdk.sourceDestination.BlockDevice> {
|
||||||
|
const sdk = await import('etcher-sdk');
|
||||||
|
|
||||||
|
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||||
|
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||||
|
await scanner.start();
|
||||||
|
let drive: sdk.sourceDestination.BlockDevice;
|
||||||
|
if (options.drive !== undefined) {
|
||||||
|
const d = scanner.getBy('device', options.drive);
|
||||||
|
if (d === undefined || !(d instanceof sdk.sourceDestination.BlockDevice)) {
|
||||||
|
throw new Error(`Drive not found: ${options.drive}`);
|
||||||
|
}
|
||||||
|
drive = d;
|
||||||
|
} else {
|
||||||
|
const driveList = new DriveList(scanner);
|
||||||
|
drive = await driveList.run();
|
||||||
|
}
|
||||||
|
scanner.stop();
|
||||||
|
return drive;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const flash: CommandDefinition<
|
||||||
|
{ image: string },
|
||||||
|
{ drive: string; yes: boolean }
|
||||||
|
> = {
|
||||||
|
signature: 'local flash <image>',
|
||||||
|
description: 'Flash an image to a drive',
|
||||||
|
//root: true,
|
||||||
|
help: stripIndent`
|
||||||
|
Use this command to flash a balenaOS image to a drive.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ balena local flash path/to/balenaos.img
|
||||||
|
$ balena local flash path/to/balenaos.img --drive /dev/disk2
|
||||||
|
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
|
||||||
|
`,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
signature: 'yes',
|
||||||
|
boolean: true,
|
||||||
|
description: 'confirm non-interactively',
|
||||||
|
alias: 'y',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: 'drive',
|
||||||
|
parameter: 'drive',
|
||||||
|
description: 'drive',
|
||||||
|
alias: 'd',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
async action(params, options) {
|
||||||
|
const visuals = await import('resin-cli-visuals');
|
||||||
|
const form = await import('resin-cli-form');
|
||||||
|
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||||
|
|
||||||
|
const drive = await getDrive(options);
|
||||||
|
|
||||||
|
const yes =
|
||||||
|
options.yes ||
|
||||||
|
(await form.ask({
|
||||||
|
message: 'This will erase the selected drive. Are you sure?',
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'yes',
|
||||||
|
default: false,
|
||||||
|
}));
|
||||||
|
if (yes !== true) {
|
||||||
|
console.log(chalk.red.bold('Aborted image flash'));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = new sourceDestination.File(
|
||||||
|
params.image,
|
||||||
|
sourceDestination.File.OpenFlags.Read,
|
||||||
|
);
|
||||||
|
|
||||||
|
const progressBars: { [key: string]: any } = {
|
||||||
|
flashing: new visuals.Progress('Flashing'),
|
||||||
|
verifying: new visuals.Progress('Validating'),
|
||||||
|
};
|
||||||
|
|
||||||
|
await multiWrite.pipeSourceToDestinations(
|
||||||
|
source,
|
||||||
|
[drive],
|
||||||
|
(_, error) => {
|
||||||
|
// onFail
|
||||||
|
console.log(chalk.red.bold(error.message));
|
||||||
|
},
|
||||||
|
(progress: sdk.multiWrite.MultiDestinationProgress) => {
|
||||||
|
// onProgress
|
||||||
|
progressBars[progress.type].update(progress);
|
||||||
|
},
|
||||||
|
true, // verify
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
###
|
###
|
||||||
|
|
||||||
exports.configure = require('./configure')
|
exports.configure = require('./configure')
|
||||||
exports.flash = require('./flash')
|
exports.flash = require('./flash').flash
|
||||||
exports.logs = require('./logs')
|
exports.logs = require('./logs')
|
||||||
exports.scan = require('./scan')
|
exports.scan = require('./scan')
|
||||||
exports.ssh = require('./ssh')
|
exports.ssh = require('./ssh')
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
###
|
|
||||||
Copyright 2016-2017 Balena
|
|
||||||
|
|
||||||
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.
|
|
||||||
###
|
|
||||||
|
|
||||||
_ = require('lodash')
|
|
||||||
|
|
||||||
exports.availableDrives =
|
|
||||||
# TODO: dedupe with https://github.com/balena-io-modules/resin-cli-visuals/blob/master/lib/widgets/drive/index.coffee
|
|
||||||
signature: 'util available-drives'
|
|
||||||
description: 'list available drives'
|
|
||||||
help: """
|
|
||||||
Use this command to list your machine's drives usable for writing the OS image to.
|
|
||||||
Skips the system drives.
|
|
||||||
"""
|
|
||||||
action: ->
|
|
||||||
Promise = require('bluebird')
|
|
||||||
drivelist = require('drivelist')
|
|
||||||
driveListAsync = Promise.promisify(drivelist.list)
|
|
||||||
chalk = require('chalk')
|
|
||||||
visuals = require('resin-cli-visuals')
|
|
||||||
|
|
||||||
formatDrive = (drive) ->
|
|
||||||
size = drive.size / 1000000000
|
|
||||||
return {
|
|
||||||
device: drive.device
|
|
||||||
size: "#{size.toFixed(1)} GB"
|
|
||||||
description: drive.description
|
|
||||||
}
|
|
||||||
|
|
||||||
getDrives = ->
|
|
||||||
driveListAsync().then (drives) ->
|
|
||||||
return _.reject(drives, system: true)
|
|
||||||
|
|
||||||
getDrives()
|
|
||||||
.then (drives) ->
|
|
||||||
if not drives.length
|
|
||||||
console.error("#{chalk.red('x')} No available drives were detected, plug one in!")
|
|
||||||
return
|
|
||||||
|
|
||||||
console.log visuals.table.horizontal drives.map(formatDrive), [
|
|
||||||
'device'
|
|
||||||
'size'
|
|
||||||
'description'
|
|
||||||
]
|
|
65
lib/actions/util.ts
Normal file
65
lib/actions/util.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* decaffeinate suggestions:
|
||||||
|
* DS102: Remove unnecessary code created because of implicit returns
|
||||||
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
Copyright 2016-2017 Balena
|
||||||
|
|
||||||
|
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 { CommandDefinition } from 'capitano';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { stripIndent } from 'common-tags';
|
||||||
|
|
||||||
|
export const availableDrives: CommandDefinition<{}, {}> = {
|
||||||
|
signature: 'util available-drives',
|
||||||
|
description: 'list available drives',
|
||||||
|
help: stripIndent`
|
||||||
|
Use this command to list your machine's drives usable for writing the OS image to.
|
||||||
|
Skips the system drives.
|
||||||
|
`,
|
||||||
|
async action() {
|
||||||
|
const sdk = await import('etcher-sdk');
|
||||||
|
const visuals = await import('resin-cli-visuals');
|
||||||
|
|
||||||
|
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||||
|
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||||
|
await scanner.start();
|
||||||
|
|
||||||
|
function formatDrive(drive: any) {
|
||||||
|
const size = drive.size / 1000000000;
|
||||||
|
return {
|
||||||
|
device: drive.device,
|
||||||
|
size: `${size.toFixed(1)} GB`,
|
||||||
|
description: drive.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanner.drives.size === 0) {
|
||||||
|
console.error(
|
||||||
|
`${chalk.red('x')} No available drives were detected, plug one in!`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
visuals.table.horizontal(Array.from(scanner.drives).map(formatDrive), [
|
||||||
|
'device',
|
||||||
|
'size',
|
||||||
|
'description',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
scanner.stop();
|
||||||
|
},
|
||||||
|
};
|
25
lib/utils/visuals/custom-dynamic-list.ts
Normal file
25
lib/utils/visuals/custom-dynamic-list.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import DynamicList = require('inquirer-dynamic-list');
|
||||||
|
|
||||||
|
export abstract class CustomDynamicList<T> extends DynamicList {
|
||||||
|
constructor(message: string, emptyMessage: string) {
|
||||||
|
super({ message, emptyMessage, choices: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract getThings(): Iterable<T>;
|
||||||
|
|
||||||
|
protected abstract format(thing: T): string;
|
||||||
|
|
||||||
|
refresh(): void {
|
||||||
|
this.opt.choices.choices = [];
|
||||||
|
this.opt.choices.realChoices = [];
|
||||||
|
for (const thing of this.getThings()) {
|
||||||
|
this.addChoice({ name: this.format(thing), value: thing });
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(): Promise<T> {
|
||||||
|
this.refresh();
|
||||||
|
return await super.run();
|
||||||
|
}
|
||||||
|
}
|
31
lib/utils/visuals/drive-list.ts
Normal file
31
lib/utils/visuals/drive-list.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import * as sdk from 'etcher-sdk';
|
||||||
|
|
||||||
|
import { CustomDynamicList } from './custom-dynamic-list';
|
||||||
|
|
||||||
|
export class DriveList extends CustomDynamicList<
|
||||||
|
sdk.sourceDestination.BlockDevice
|
||||||
|
> {
|
||||||
|
constructor(private scanner: sdk.scanner.Scanner) {
|
||||||
|
super(
|
||||||
|
'Select a drive',
|
||||||
|
`${chalk.red('x')} No available drives were detected, plug one in!`,
|
||||||
|
);
|
||||||
|
const refresh = this.refresh.bind(this);
|
||||||
|
scanner.on('attach', refresh);
|
||||||
|
scanner.on('detach', refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected *getThings() {
|
||||||
|
for (const drive of this.scanner.drives) {
|
||||||
|
if (drive instanceof sdk.sourceDestination.BlockDevice) {
|
||||||
|
yield drive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected format(drive: sdk.sourceDestination.BlockDevice) {
|
||||||
|
const size = drive.size / 1e9;
|
||||||
|
return `${drive.device} (${size.toFixed(1)} GB) - ${drive.description}`;
|
||||||
|
}
|
||||||
|
}
|
@ -126,9 +126,8 @@
|
|||||||
"docker-toolbelt": "^3.3.5",
|
"docker-toolbelt": "^3.3.5",
|
||||||
"dockerode": "^2.5.5",
|
"dockerode": "^2.5.5",
|
||||||
"dockerode-options": "^0.2.1",
|
"dockerode-options": "^0.2.1",
|
||||||
"drivelist": "^5.0.22",
|
|
||||||
"ejs": "^2.5.7",
|
"ejs": "^2.5.7",
|
||||||
"etcher-image-write": "^9.0.3",
|
"etcher-sdk": "^0.2.0",
|
||||||
"event-stream": "3.3.4",
|
"event-stream": "3.3.4",
|
||||||
"express": "^4.13.3",
|
"express": "^4.13.3",
|
||||||
"global-tunnel-ng": "^2.1.1",
|
"global-tunnel-ng": "^2.1.1",
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./typings/*.d.ts",
|
"./typings/*.d.ts",
|
||||||
|
"./node_modules/etcher-sdk/typings/**/*.d.ts",
|
||||||
"./lib/**/*.ts"
|
"./lib/**/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
26
typings/inquire-dynamic-list.d.ts
vendored
Normal file
26
typings/inquire-dynamic-list.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
declare module 'inquirer-dynamic-list' {
|
||||||
|
interface Choice {
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DynamicList {
|
||||||
|
opt: {
|
||||||
|
choices: {
|
||||||
|
choices: Choice[];
|
||||||
|
realChoices: Choice[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(options: {
|
||||||
|
message?: string;
|
||||||
|
emptyMessage?: string;
|
||||||
|
choices: Choice[];
|
||||||
|
});
|
||||||
|
addChoice(choice: Choice): void;
|
||||||
|
render(): void;
|
||||||
|
run(): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export = DynamicList;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user