isolate the sudo-runnable command

This commit is contained in:
Eugene Mirotin 2017-03-24 12:48:14 +03:00
parent 857c5204b9
commit 6cf32e445a
16 changed files with 244 additions and 121 deletions

View File

@ -291,23 +291,23 @@ exports.init = {
}).then(resin.models.application.get).then(function(application) { }).then(resin.models.application.get).then(function(application) {
var download; var download;
download = function() { download = function() {
return tmp.tmpNameAsync().then(function(temporalPath) { return tmp.tmpNameAsync().then(function(tempPath) {
return capitano.runAsync("os download " + application.device_type + " --output " + temporalPath + " --version default"); return capitano.runAsync("os download " + application.device_type + " --output '" + tempPath + "' --version default");
}).disposer(function(temporalPath) { }).disposer(function(tempPath) {
return rimraf(temporalPath); return rimraf(tempPath);
}); });
}; };
return Promise.using(download(), function(temporalPath) { return Promise.using(download(), function(tempPath) {
return capitano.runAsync("device register " + application.app_name).then(resin.models.device.get).tap(function(device) { return capitano.runAsync("device register " + application.app_name).then(resin.models.device.get).tap(function(device) {
var configure; var configureCommand;
configure = "os configure " + temporalPath + " " + device.uuid; configureCommand = "os configure '" + tempPath + "' " + device.uuid;
if (options.advanced) { if (options.advanced) {
configure += ' --advanced'; configureCommand += ' --advanced';
} }
return capitano.runAsync(configure).then(function() { return capitano.runAsync(configureCommand).then(function() {
var message; var osInitCommand;
message = 'Initializing a device requires administrative permissions\ngiven that we need to access raw devices directly.\n'; osInitCommand = "os initialize '" + tempPath + "' --type " + application.device_type;
return helpers.sudo(['os', 'initialize', temporalPath, '--type', application.device_type], message); return capitano.runAsync(osInitCommand);
})["catch"](function(error) { })["catch"](function(error) {
return resin.models.device.remove(device.uuid)["finally"](function() { return resin.models.device.remove(device.uuid)["finally"](function() {
throw error; throw error;

View File

@ -60,12 +60,13 @@ general = function(params, options, done) {
console.log(messages.reachingOut); console.log(messages.reachingOut);
console.log('\nPrimary commands:\n'); console.log('\nPrimary commands:\n');
commands = _.reject(capitano.state.commands, function(command) { commands = _.reject(capitano.state.commands, function(command) {
return command.isWildcard(); return command.hidden || command.isWildcard();
}); });
groupedCommands = _.groupBy(commands, function(command) { groupedCommands = _.groupBy(commands, function(command) {
if (command.plugin) { if (command.plugin) {
return 'plugins'; return 'plugins';
} else if (command.primary) { }
if (command.primary) {
return 'primary'; return 'primary';
} }
return 'secondary'; return 'secondary';

View File

@ -31,5 +31,6 @@ module.exports = {
settings: require('./settings'), settings: require('./settings'),
config: require('./config'), config: require('./config'),
sync: require('./sync'), sync: require('./sync'),
ssh: require('./ssh') ssh: require('./ssh'),
internal: require('./internal')
}; };

35
build/actions/internal.js Normal file
View File

@ -0,0 +1,35 @@
// Generated by CoffeeScript 1.12.4
/*
Copyright 2016 Resin.io
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.
*/
exports.osInit = {
signature: 'internal osinit <image> <type> <config>',
description: 'do actual init of the device with the preconfigured os image',
help: 'Don\'t use this command directly! Use `resin os initialize <image>` instead.',
hidden: true,
root: true,
action: function(params, options, done) {
var Promise, helpers, init;
Promise = require('bluebird');
init = require('resin-device-init');
helpers = require('../utils/helpers');
return Promise["try"](function() {
var config;
config = JSON.parse(params.config);
return init.initialize(params.image, params.type, config);
}).then(helpers.osProgressHandler).nodeify(done);
}
};

View File

@ -15,7 +15,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var commandOptions, formatVersion, resolveVersion, stepHandler; var commandOptions, formatVersion, initWarningMessage, resolveVersion;
commandOptions = require('./command-options'); commandOptions = require('./command-options');
@ -120,25 +120,6 @@ exports.download = {
} }
}; };
stepHandler = function(step) {
var _, bar, helpers, rindle, visuals;
_ = require('lodash');
rindle = require('rindle');
visuals = require('resin-cli-visuals');
helpers = require('../utils/helpers');
step.on('stdout', _.bind(process.stdout.write, process.stdout));
step.on('stderr', _.bind(process.stderr.write, process.stderr));
step.on('state', function(state) {
if (state.operation.command === 'burn') {
return;
}
return console.log(helpers.stateToString(state));
});
bar = new visuals.Progress('Writing Device OS');
step.on('burn', _.bind(bar.update, bar));
return rindle.wait(step);
};
exports.configure = { exports.configure = {
signature: 'os configure <image> <uuid>', signature: 'os configure <image> <uuid>',
description: 'configure an os image', description: 'configure an os image',
@ -176,16 +157,18 @@ exports.configure = {
override: override override: override
}); });
}).then(function(answers) { }).then(function(answers) {
return init.configure(params.image, params.uuid, answers).then(stepHandler); return init.configure(params.image, params.uuid, answers).then(helpers.osProgressHandler);
}); });
}).nodeify(done); }).nodeify(done);
} }
}; };
initWarningMessage = 'Note: Initializing the device may ask for administrative permissions\nbecause we need to access the raw devices directly.';
exports.initialize = { exports.initialize = {
signature: 'os initialize <image>', signature: 'os initialize <image>',
description: 'initialize an os image', description: 'initialize an os image',
help: 'Use this command to initialize a previously configured operating system image.\n\nExamples:\n\n $ resin os initialize ../path/rpi.img --type \'raspberry-pi\'', help: "Use this command to initialize a device with previously configured operating system image.\n\n" + initWarningMessage + "\n\nExamples:\n\n $ resin os initialize ../path/rpi.img --type 'raspberry-pi'",
permission: 'user', permission: 'user',
options: [ options: [
commandOptions.yes, { commandOptions.yes, {
@ -201,16 +184,14 @@ exports.initialize = {
alias: 'd' alias: 'd'
} }
], ],
root: true,
action: function(params, options, done) { action: function(params, options, done) {
var Promise, form, helpers, init, patterns, umount; var Promise, form, helpers, patterns, umount;
Promise = require('bluebird'); Promise = require('bluebird');
umount = Promise.promisifyAll(require('umount')); umount = Promise.promisifyAll(require('umount'));
form = require('resin-cli-form'); form = require('resin-cli-form');
init = require('resin-device-init');
patterns = require('../utils/patterns'); patterns = require('../utils/patterns');
helpers = require('../utils/helpers'); helpers = require('../utils/helpers');
console.info('Initializing device'); console.info("Initializing device\n\n" + initWarningMessage);
return helpers.getManifest(params.image, options.type).then(function(manifest) { return helpers.getManifest(params.image, options.type).then(function(manifest) {
var ref; var ref;
return (ref = manifest.initialization) != null ? ref.options : void 0; return (ref = manifest.initialization) != null ? ref.options : void 0;
@ -228,7 +209,7 @@ exports.initialize = {
message = "This will erase " + answers.drive + ". Are you sure?"; message = "This will erase " + answers.drive + ". Are you sure?";
return patterns.confirm(options.yes, message)["return"](answers.drive).then(umount.umountAsync); return patterns.confirm(options.yes, message)["return"](answers.drive).then(umount.umountAsync);
}).tap(function(answers) { }).tap(function(answers) {
return init.initialize(params.image, options.type, answers).then(stepHandler); return helpers.sudo(['internal', 'osinit', params.image, options.type, JSON.stringify(answers)]);
}).then(function(answers) { }).then(function(answers) {
if (answers.drive == null) { if (answers.drive == null) {
return; return;

35
build/actions/util.js Normal file
View File

@ -0,0 +1,35 @@
// Generated by CoffeeScript 1.12.4
/*
Copyright 2016 Resin.io
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.
*/
exports.osInit = {
signature: 'util osinit <image> <type> <config>',
description: 'do actual init of the device with the preconfigured os image',
help: 'Don\'t use this command directly! Use `resin os initialize <image>` instead.',
hidden: true,
root: true,
action: function(params, options, done) {
var Promise, helpers, init;
Promise = require('bluebird');
init = require('resin-device-init');
helpers = require('../utils/helpers');
return Promise["try"](function() {
var config;
config = JSON.parse(params.config);
return init.initialize(params.image, params.type, config);
}).then(helpers.osProgressHandler).nodeify(done);
}
};

View File

@ -179,6 +179,8 @@ capitano.command(actions.local.scan);
capitano.command(actions.local.stop); capitano.command(actions.local.stop);
capitano.command(actions.internal.osInit);
update.notify(); update.notify();
plugins.register(/^resin-plugin-(.+)$/).then(function() { plugins.register(/^resin-plugin-(.+)$/).then(function() {

View File

@ -15,37 +15,27 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var Promise, _, capitano, chalk, imagefs, os, president, resin, rindle; var Promise, capitano, president;
Promise = require('bluebird'); Promise = require('bluebird');
capitano = Promise.promisifyAll(require('capitano')); capitano = Promise.promisifyAll(require('capitano'));
_ = require('lodash');
_.str = require('underscore.string');
president = Promise.promisifyAll(require('president')); president = Promise.promisifyAll(require('president'));
resin = require('resin-sdk-preconfigured');
imagefs = require('resin-image-fs');
rindle = require('rindle');
os = require('os');
chalk = require('chalk');
exports.getGroupDefaults = function(group) { exports.getGroupDefaults = function(group) {
var _;
_ = require('lodash');
return _.chain(group).get('options').map(function(question) { return _.chain(group).get('options').map(function(question) {
return [question.name, question["default"]]; return [question.name, question["default"]];
}).object().value(); }).object().value();
}; };
exports.stateToString = function(state) { exports.stateToString = function(state) {
var percentage, result; var _str, chalk, percentage, result;
percentage = _.str.lpad(state.percentage, 3, '0') + '%'; _str = require('underscore.string');
chalk = require('chalk');
percentage = _str.lpad(state.percentage, 3, '0') + '%';
result = (chalk.blue(percentage)) + " " + (chalk.cyan(state.operation.command)); result = (chalk.blue(percentage)) + " " + (chalk.cyan(state.operation.command));
switch (state.operation.command) { switch (state.operation.command) {
case 'copy': case 'copy':
@ -59,16 +49,22 @@ exports.stateToString = function(state) {
} }
}; };
exports.sudo = function(command, message) { exports.sudo = function(command) {
command = _.union(_.take(process.argv, 2), command); var _, os;
console.log(message); _ = require('lodash');
os = require('os');
if (os.platform() !== 'win32') { if (os.platform() !== 'win32') {
console.log('Type your computer password to continue'); console.log('If asked please type your computer password to continue');
} }
command = _.union(_.take(process.argv, 2), command);
return president.executeAsync(command); return president.executeAsync(command);
}; };
exports.getManifest = function(image, deviceType) { exports.getManifest = function(image, deviceType) {
var imagefs, resin, rindle;
rindle = require('rindle');
imagefs = require('resin-image-fs');
resin = require('resin-sdk-preconfigured');
return imagefs.read({ return imagefs.read({
image: image, image: image,
partition: { partition: {
@ -79,3 +75,20 @@ exports.getManifest = function(image, deviceType) {
return resin.models.device.getManifestBySlug(deviceType); return resin.models.device.getManifestBySlug(deviceType);
}); });
}; };
exports.osProgressHandler = function(step) {
var bar, rindle, visuals;
rindle = require('rindle');
visuals = require('resin-cli-visuals');
step.on('stdout', process.stdout.write.bind(process.stdout));
step.on('stderr', process.stderr.write.bind(process.stderr));
step.on('state', function(state) {
if (state.operation.command === 'burn') {
return;
}
return console.log(exports.stateToString(state));
});
bar = new visuals.Progress('Writing Device OS');
step.on('burn', bar.update.bind(bar));
return rindle.wait(step);
};

View File

@ -830,7 +830,10 @@ show advanced commands
## os initialize &#60;image&#62; ## os initialize &#60;image&#62;
Use this command to initialize a previously configured operating system image. Use this command to initialize a device with previously configured operating system image.
Note: Initializing the device may ask for administrative permissions
because we need to access the raw devices directly.
Examples: Examples:

View File

@ -392,27 +392,23 @@ exports.init =
.then (application) -> .then (application) ->
download = -> download = ->
tmp.tmpNameAsync().then (temporalPath) -> tmp.tmpNameAsync().then (tempPath) ->
# TODO: allow version selection # TODO: allow version selection
capitano.runAsync("os download #{application.device_type} --output #{temporalPath} --version default") capitano.runAsync("os download #{application.device_type} --output '#{tempPath}' --version default")
.disposer (temporalPath) -> .disposer (tempPath) ->
return rimraf(temporalPath) return rimraf(tempPath)
Promise.using download(), (temporalPath) -> Promise.using download(), (tempPath) ->
capitano.runAsync("device register #{application.app_name}") capitano.runAsync("device register #{application.app_name}")
.then(resin.models.device.get) .then(resin.models.device.get)
.tap (device) -> .tap (device) ->
configure = "os configure #{temporalPath} #{device.uuid}" configureCommand = "os configure '#{tempPath}' #{device.uuid}"
configure += ' --advanced' if options.advanced if options.advanced
capitano.runAsync(configure).then -> configureCommand += ' --advanced'
message = ''' capitano.runAsync(configureCommand)
Initializing a device requires administrative permissions .then ->
given that we need to access raw devices directly. osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
capitano.runAsync(osInitCommand)
'''
helpers.sudo([ 'os', 'initialize', temporalPath, '--type', application.device_type ], message)
# Make sure the device resource is removed if there is an # Make sure the device resource is removed if there is an
# error when configuring or initializing a device image # error when configuring or initializing a device image
.catch (error) -> .catch (error) ->

View File

@ -54,12 +54,12 @@ general = (params, options, done) ->
# We do not want the wildcard command # We do not want the wildcard command
# to be printed in the help screen. # to be printed in the help screen.
commands = _.reject capitano.state.commands, (command) -> commands = _.reject capitano.state.commands, (command) ->
return command.isWildcard() return command.hidden or command.isWildcard()
groupedCommands = _.groupBy commands, (command) -> groupedCommands = _.groupBy commands, (command) ->
if command.plugin if command.plugin
return 'plugins' return 'plugins'
else if command.primary if command.primary
return 'primary' return 'primary'
return 'secondary' return 'secondary'

View File

@ -31,3 +31,4 @@ module.exports =
config: require('./config') config: require('./config')
sync: require('./sync') sync: require('./sync')
ssh: require('./ssh') ssh: require('./ssh')
internal: require('./internal')

View File

@ -0,0 +1,37 @@
###
Copyright 2016 Resin.io
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.
###
# These are internal commands we want to be runnable from the outside
# One use-case for this is spawning the minimal operation with root priviledges
exports.osInit =
signature: 'internal osinit <image> <type> <config>'
description: 'do actual init of the device with the preconfigured os image'
help: '''
Don't use this command directly! Use `resin os initialize <image>` instead.
'''
hidden: true
root: true
action: (params, options, done) ->
Promise = require('bluebird')
init = require('resin-device-init')
helpers = require('../utils/helpers')
return Promise.try ->
config = JSON.parse(params.config)
init.initialize(params.image, params.type, config)
.then(helpers.osProgressHandler)
.nodeify(done)

View File

@ -133,25 +133,6 @@ exports.download =
console.info('The image was downloaded successfully') console.info('The image was downloaded successfully')
.nodeify(done) .nodeify(done)
stepHandler = (step) ->
_ = require('lodash')
rindle = require('rindle')
visuals = require('resin-cli-visuals')
helpers = require('../utils/helpers')
step.on('stdout', _.bind(process.stdout.write, process.stdout))
step.on('stderr', _.bind(process.stderr.write, process.stderr))
step.on 'state', (state) ->
return if state.operation.command is 'burn'
console.log(helpers.stateToString(state))
bar = new visuals.Progress('Writing Device OS')
step.on('burn', _.bind(bar.update, bar))
return rindle.wait(step)
exports.configure = exports.configure =
signature: 'os configure <image> <uuid>' signature: 'os configure <image> <uuid>'
description: 'configure an os image' description: 'configure an os image'
@ -192,19 +173,26 @@ exports.configure =
return form.run(questions, { override }) return form.run(questions, { override })
.then (answers) -> .then (answers) ->
init.configure(params.image, params.uuid, answers).then(stepHandler) init.configure(params.image, params.uuid, answers).then(helpers.osProgressHandler)
.nodeify(done) .nodeify(done)
initWarningMessage = '''
Note: Initializing the device may ask for administrative permissions
because we need to access the raw devices directly.
'''
exports.initialize = exports.initialize =
signature: 'os initialize <image>' signature: 'os initialize <image>'
description: 'initialize an os image' description: 'initialize an os image'
help: ''' help: """
Use this command to initialize a previously configured operating system image. Use this command to initialize a device with previously configured operating system image.
#{initWarningMessage}
Examples: Examples:
$ resin os initialize ../path/rpi.img --type 'raspberry-pi' $ resin os initialize ../path/rpi.img --type 'raspberry-pi'
''' """
permission: 'user' permission: 'user'
options: [ options: [
commandOptions.yes commandOptions.yes
@ -222,16 +210,18 @@ exports.initialize =
alias: 'd' alias: 'd'
} }
] ]
root: true
action: (params, options, done) -> action: (params, options, done) ->
Promise = require('bluebird') Promise = require('bluebird')
umount = Promise.promisifyAll(require('umount')) umount = Promise.promisifyAll(require('umount'))
form = require('resin-cli-form') form = require('resin-cli-form')
init = require('resin-device-init')
patterns = require('../utils/patterns') patterns = require('../utils/patterns')
helpers = require('../utils/helpers') helpers = require('../utils/helpers')
console.info('Initializing device') console.info("""
Initializing device
#{initWarningMessage}
""")
helpers.getManifest(params.image, options.type) helpers.getManifest(params.image, options.type)
.then (manifest) -> .then (manifest) ->
return manifest.initialization?.options return manifest.initialization?.options
@ -246,7 +236,13 @@ exports.initialize =
.return(answers.drive) .return(answers.drive)
.then(umount.umountAsync) .then(umount.umountAsync)
.tap (answers) -> .tap (answers) ->
return init.initialize(params.image, options.type, answers).then(stepHandler) return helpers.sudo([
'internal'
'osinit'
params.image
options.type
JSON.stringify(answers)
])
.then (answers) -> .then (answers) ->
return if not answers.drive? return if not answers.drive?
umount.umountAsync(answers.drive).tap -> umount.umountAsync(answers.drive).tap ->

View File

@ -142,6 +142,9 @@ capitano.command(actions.local.ssh)
capitano.command(actions.local.scan) capitano.command(actions.local.scan)
capitano.command(actions.local.stop) capitano.command(actions.local.stop)
# ---------- Internal utils ----------
capitano.command(actions.internal.osInit)
update.notify() update.notify()
plugins.register(/^resin-plugin-(.+)$/).then -> plugins.register(/^resin-plugin-(.+)$/).then ->

View File

@ -16,16 +16,11 @@ limitations under the License.
Promise = require('bluebird') Promise = require('bluebird')
capitano = Promise.promisifyAll(require('capitano')) capitano = Promise.promisifyAll(require('capitano'))
_ = require('lodash')
_.str = require('underscore.string')
president = Promise.promisifyAll(require('president')) president = Promise.promisifyAll(require('president'))
resin = require('resin-sdk-preconfigured')
imagefs = require('resin-image-fs')
rindle = require('rindle')
os = require('os')
chalk = require('chalk')
exports.getGroupDefaults = (group) -> exports.getGroupDefaults = (group) ->
_ = require('lodash')
return _.chain(group) return _.chain(group)
.get('options') .get('options')
.map (question) -> .map (question) ->
@ -34,7 +29,10 @@ exports.getGroupDefaults = (group) ->
.value() .value()
exports.stateToString = (state) -> exports.stateToString = (state) ->
percentage = _.str.lpad(state.percentage, 3, '0') + '%' _str = require('underscore.string')
chalk = require('chalk')
percentage = _str.lpad(state.percentage, 3, '0') + '%'
result = "#{chalk.blue(percentage)} #{chalk.cyan(state.operation.command)}" result = "#{chalk.blue(percentage)} #{chalk.cyan(state.operation.command)}"
switch state.operation.command switch state.operation.command
@ -47,16 +45,20 @@ exports.stateToString = (state) ->
else else
throw new Error("Unsupported operation: #{state.operation.type}") throw new Error("Unsupported operation: #{state.operation.type}")
exports.sudo = (command, message) -> exports.sudo = (command) ->
command = _.union(_.take(process.argv, 2), command) _ = require('lodash')
console.log(message) os = require('os')
if os.platform() isnt 'win32' if os.platform() isnt 'win32'
console.log('Type your computer password to continue') console.log('If asked please type your computer password to continue')
command = _.union(_.take(process.argv, 2), command)
return president.executeAsync(command) return president.executeAsync(command)
exports.getManifest = (image, deviceType) -> exports.getManifest = (image, deviceType) ->
rindle = require('rindle')
imagefs = require('resin-image-fs')
resin = require('resin-sdk-preconfigured')
# Attempt to read manifest from the first # Attempt to read manifest from the first
# partition, but fallback to the API if # partition, but fallback to the API if
@ -70,3 +72,20 @@ exports.getManifest = (image, deviceType) ->
.then(JSON.parse) .then(JSON.parse)
.catch -> .catch ->
resin.models.device.getManifestBySlug(deviceType) resin.models.device.getManifestBySlug(deviceType)
exports.osProgressHandler = (step) ->
rindle = require('rindle')
visuals = require('resin-cli-visuals')
step.on('stdout', process.stdout.write.bind(process.stdout))
step.on('stderr', process.stderr.write.bind(process.stderr))
step.on 'state', (state) ->
return if state.operation.command is 'burn'
console.log(exports.stateToString(state))
bar = new visuals.Progress('Writing Device OS')
step.on('burn', bar.update.bind(bar))
return rindle.wait(step)