Skip to content

Commit

Permalink
Adds exit callbacks, scpOptions, config task command line arguments r…
Browse files Browse the repository at this point in the history
…ewriting, and example controller script demonstrating usage of the new features.
  • Loading branch information
tsmith committed Jun 10, 2011
1 parent f15191a commit 01a79ba
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 19 deletions.
5 changes: 5 additions & 0 deletions 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

Expand Down
82 changes: 73 additions & 9 deletions README
Expand Up @@ -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.



Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
72 changes: 72 additions & 0 deletions 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
25 changes: 18 additions & 7 deletions lib/host.js
Expand Up @@ -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);
Expand All @@ -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);
}
}
});
}
Expand All @@ -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');
}
Expand All @@ -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');
}
Expand All @@ -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');
}
Expand Down
5 changes: 2 additions & 3 deletions lib/task.js
Expand Up @@ -30,7 +30,6 @@ function perform(name, config) {

sys.puts(log);


if (!task) {
throw new Error('No task named: ' + name);
}
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 01a79ba

Please sign in to comment.