Permalink
Browse files

Adds exit callbacks, scpOptions, config task command line arguments r…

…ewriting, and example controller script demonstrating usage of the new features.
  • Loading branch information...
1 parent f15191a commit 01a79bac1fc46bd86d60abfdfe757bd996c3cf3e @tsmith committed Jun 10, 2011
Showing with 170 additions and 19 deletions.
  1. +5 −0 CHANGELOG
  2. +73 −9 README
  3. +72 −0 example/mycontroller.js
  4. +18 −7 lib/host.js
  5. +2 −3 lib/task.js
View
5 CHANGELOG
@@ -1,3 +1,8 @@
+0.1.8
+- Add exit callbacks to host.ssh(), host.scp() for handling non-zero exit codes
+- Add scpOptions (like sshOptions)
+- Add config tasks command line arguments rewriting
+
0.1.7
- Add host.logMask to allow masking things like passwords from command logging
View
82 README
@@ -161,23 +161,38 @@ task('date', 'Get date', function (host) {
The host object allows access to all the properties defined in the config and
-also provides the ssh and scp methods for communicating with the remote
+also provides the ssh() and scp() methods for communicating with the remote
machine.
-The ssh method takes one argument - the command to be executed on the
+The ssh() method takes one argument - the command to be executed on the
remote machine. The scp method takes two arguments - the local file path and the
remote file path.
-Both ssh and scp methods are asynchronous and can additionally take a callback
-function that is executed once the ssh or scp operation is complete. This
-guarantees that the first operation completes before the next one begins on
-that host:
+Both ssh() and scp() methods are asynchronous and can additionally take a
+callback function that is executed once the ssh or scp operation is complete.
+This guarantees that the first operation completes before the next one begins
+on that host:
host.scp(release, remoteDir, function () {
host.ssh('tar xzvf ' + remotePath + ' -C ' + remoteDir, function () {
-Callbacks can be chained as far as necessary.
+You can chain callbacks as far as necessary.
+
+If a command returns a non-zero exit code, the scp() and ssh() methods will log
+the exit and exit code, but will not call the callback, ending any further
+operations on that host. This avoids doing further harm where a callback may
+assume a successful execution of a previous command. However, you can specify
+an exit callback that will receive the exit code if a non-zero exit occurs:
+
+function callback() { ... }
+function exitCallback(code) { ... }
+
+host.ssh('date', callback, exitCallback);
+
+
+You can make both callbacks the same callback function if you want to check the
+exit code and handle a non-zero exit within a single callback.
@@ -340,6 +355,7 @@ task('mycluster', 'Config for my cluster', function () {
return control.hosts(config, addresses);
});
+
Since each host gets its own log property, every host could conceivably have
its own log fie. However, every line in the log file has a prefix that includes
the host address so, for example:
@@ -388,14 +404,62 @@ options to the host object sshOptions property prior to executing the command:
// Config task
config = {
user: 'mylogin',
- sshOptions = [ '-2', '-p 44' ]
+ sshOptions: [ '-2', '-p 44' ]
};
// Work task
host.sshOptions = host.sshOptions.concat('-v');
+Use scpOptions in the same manner to pass options to the scp command when using
+scp().
+
+
+
+CONFIG TASK COMMAND LINE ARGUMENTS REWRITING
+
+Config tasks receive a reference to the array of remaining arguments on the
+command line after the config task name is removed. This allows config tasks
+to rewrite the command line arguments other than the config task name. Example:
+
+function configure(addresses) {
+ var config;
+ config = {
+ user: 'mylogin'
+ };
+ return control.hosts(config, addresses);
+}
+
+task('mycluster', 'Config for my cluster', function () {
+ var addresses = [ 'a.mydomain.com',
+ 'b.mydomain.com',
+ 'c.mydomain.com' ];
+ return configure(addresses);
+});
+
+task('myhost', 'Config for a single host from command line', function (args) {
+ return configure([args.shift()]); // From command line arguments rewriting
+});
+
+
+With this set of config tasks, if there is an ad hoc need to run certain tasks
+against a single machine in the cluster, but otherwise have identical
+configuration as when run as part of the cluster, the host name can be
+specified on the command line:
+
+node mycontroller.js myhost b.mydomain.com date a
+
+
+In that case, the myhost task receives as args:
+
+['b.mydomain.com', 'date', 'a']
+
+
+This is generally discouraged in favor of editing the config file as needed,
+but is available if config tasks need to have command line arguments or rewrite
+the work task name and its arguments on the fly.
+
FEEDBACK
-Feedback welcome at node@thomassmith.com or the Node mailing list.
+Welcome at node@thomassmith.com or the Node mailing list.
View
72 example/mycontroller.js
@@ -0,0 +1,72 @@
+/*global require, process, console */
+
+// Example with some advanced usage, like error callbacks, scpOptions,
+// and config task command line arguments rewriting, using localhost as
+// a 'remote' machine.
+
+var control = require('../'),
+ task = control.task,
+ script = process.argv[1],
+ scpTest = 'controlScpTest';
+
+function configure(addresses) {
+ var config;
+ config = {
+ user: process.env.USER,
+ scpOptions: ['-v']
+ };
+ return control.hosts(config, addresses);
+}
+
+task('mycluster', 'Config for my cluster', function () {
+ return configure([ 'localhost' ]); // Expand array to create cluster
+});
+
+task('myhost', 'Config for a single host from command line', function (args) {
+ return configure([args.shift()]); // From command line arguments rewriting
+});
+
+function doTest(host, code, callback, exitCallback) {
+ code = code || 0;
+ host.ssh('node ' + script + ' mycluster arbexit ' + code,
+ callback, exitCallback);
+}
+
+// Task to perform 'remote' call requesting 'remote' to exit arbitrarily
+task('test', 'Test task', function (host, code) {
+
+ function callback() {
+ console.log('Regular callback activated for ' + host.address);
+ }
+
+ function exitCallback(exit) {
+ console.log('Exit callback activated for ' + host.address +
+ ' with exit code ' + exit);
+ }
+
+ doTest(host, code, callback, exitCallback);
+});
+
+// Task that will run on 'remote' to exit with an arbitrary code
+task('arbexit', 'Arbitrary exit', function (host, code) {
+ code = code || 0;
+ console.log("Exiting with code " + code);
+ process.exit(code);
+});
+
+task('scp', 'Test scp options', function (host) {
+ host.scp(script, scpTest);
+});
+
+task('clean', 'Remove file transferred in scp testing', function (host) {
+ host.ssh('rm ' + scpTest);
+});
+
+control.begin();
+
+// Run like:
+// node mycontroller.js mycluster test 0
+// node mycontroller.js mycluster test 64
+// node mycontroller.js mycluster scp
+// node mycontroller.js mycluster clean
+// node mycontroller.js myhost 127.0.0.1 test 0
View
25 lib/host.js
@@ -9,7 +9,7 @@ function logBuffer(log, prefix, buffer) {
log.puts(message, prefix);
}
-function listen(subProcess, log, callback) {
+function listen(subProcess, log, callback, exitCallback) {
var codes = '';
subProcess.stdout.addListener('data', function (data) {
logBuffer(log, 'stdout: ', data);
@@ -21,8 +21,14 @@ function listen(subProcess, log, callback) {
subProcess.addListener('exit', function (code) {
logBuffer(log, 'exit: ', code);
- if (code === 0 && callback) {
- callback(this);
+ if (code === 0) {
+ if (callback) {
+ callback();
+ }
+ } else {
+ if (exitCallback) {
+ exitCallback(code);
+ }
}
});
}
@@ -36,7 +42,7 @@ function star(mask) {
return stars;
}
-function ssh(command, callback) {
+function ssh(command, callback, exitCallback) {
if (!command) {
throw new Error(this.address + ': No command to run');
}
@@ -61,10 +67,10 @@ function ssh(command, callback) {
log.puts(user + ':ssh ' + command);
subProcess = spawn('ssh', args);
- listen(subProcess, log, callback);
+ listen(subProcess, log, callback, exitCallback);
}
-function scp(local, remote, callback) {
+function scp(local, remote, callback, exitCallback) {
if (!local) {
throw new Error(this.address + ': No local file path');
}
@@ -75,16 +81,21 @@ function scp(local, remote, callback) {
var log = this.logger,
user = this.user,
+ options = this.scpOptions,
address = this.address;
path.exists(local, function (exists) {
if (exists) {
var reference = user + '@' + address + ':' + remote,
args = ['-r', local, reference],
subProcess;
+ if (options) {
+ args = options.concat(args);
+ }
+
log.puts(user + ':scp: ' + local + ' ' + reference);
subProcess = spawn('scp', args);
- listen(subProcess, log, callback);
+ listen(subProcess, log, callback, exitCallback);
} else {
throw new Error('Local: ' + local + ' does not exist');
}
View
5 lib/task.js
@@ -30,7 +30,6 @@ function perform(name, config) {
sys.puts(log);
-
if (!task) {
throw new Error('No task named: ' + name);
}
@@ -75,8 +74,8 @@ function list() {
function begin() {
var configTask = process.argv[2],
- configs = perform(configTask),
- taskWithArgs = process.argv.slice(3);
+ taskWithArgs = process.argv.slice(3),
+ configs = perform(configTask, taskWithArgs);
if (taskWithArgs.length > 0) {
taskWithArgs.unshift(configs);
performAll.apply(null, taskWithArgs);

0 comments on commit 01a79ba

Please sign in to comment.