Merge pull request #1052 from balena-io/update-local-flash-action

Update "local flash" and "util available-drives" actions
This commit is contained in:
Alexis Svinartchouk 2019-01-11 18:22:04 +01:00 committed by GitHub
commit 5cf0f7030d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 271 additions and 179 deletions

View File

@ -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
View 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
);
},
};

View File

@ -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')

View File

@ -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
View 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();
},
};

View 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();
}
}

View 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}`;
}
}

View File

@ -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",

View File

@ -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
View 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;
}