diff --git a/bin/install.js b/bin/install.js new file mode 100755 index 00000000..adaf69ca --- /dev/null +++ b/bin/install.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +var nodeControl = require('../deps/node-control/index.js'); +var util = require('util'); +var ins = util.inspect; +var config = { user: process.env.USER }; + +var print_lines_prefix = function (prefix, lines) { + var i = 0, l = lines.length; + for(i = 0; i < l; i++) { + if (i < (l - 1) || lines[i].length > 0) console.log('%s: %s', prefix, lines[i]); + } +} + +var hosts = nodeControl.hosts(config, ['node01', 'node02', 'node03', 'node04']); +var l = hosts.length, + i = 0; +for(i = 0; i < l; i++) { + (function () { + + var my_i = i; + var my_host = hosts[my_i]; + my_host.ssh('hostname -s', my_host.address, function (err, stdout, stderr) { + if (err) { + console.error('host %d error: %s', my_i, err.toString()); + } + if (stdout.length > 0) print_lines_prefix(my_host.address, stdout.split('\n')); + if (stderr.length > 0) print_lines_prefix(my_host.address + ' ERROR: ', stderr.split('\n')); + }); + })(); +} \ No newline at end of file diff --git a/deps/node-control/CHANGELOG b/deps/node-control/CHANGELOG new file mode 100644 index 00000000..6187a589 --- /dev/null +++ b/deps/node-control/CHANGELOG @@ -0,0 +1,25 @@ +0.1.9 +- Init scpOptions as array if not configured so later logic can assume array +- Document and add example of hosts() usage without tasks system + +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 + +0.1.6 +- Document sshOptions and reimplement to allow setting in config or host object +- Remove undocumented host() method for setting up a single host from config +- Add engines specfication to package.json for node >=0.1.99 + +0.1.5 +- Remove errant console.log statement in ssh command +- Log commands before launching subprocess instead of after + +0.1.4 +- Add sshOptions to host config to allow passing options to ssh on ssh() +- Add -r flag to scp() invocations to make useful for both files and directories +- Tighten permissions on log file so readable only to local control user diff --git a/deps/node-control/LICENSE b/deps/node-control/LICENSE new file mode 100644 index 00000000..646bb6e6 --- /dev/null +++ b/deps/node-control/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010 Thomas Smith (MIT License) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/deps/node-control/README b/deps/node-control/README new file mode 100644 index 00000000..d318098c --- /dev/null +++ b/deps/node-control/README @@ -0,0 +1,494 @@ +DESCRIPTION + +Use node-control to define ssh and scp tasks for system administration and code +deployment and execute them on one or more machines simultaneously. The +implementation and API is completely asynchronous, allowing coding of tasks +according to Node conventions and control of many machines in parallel. +Strong logging creates a complete audit trail of commands executed on remote +machines in logs easily analyzed by standard text manipulation tools. + +The only dependency is OpenSSH and Node on the local control machine. Remote +machines simply need to have a standard sshd daemon running. + + + +QUICK EXAMPLE + +If you want to control remote machines from individual scripts without the +tasks system, see QUICK EXAMPLE WITHOUT TASKS below. Otherwise, to get the +current date from the three machines listed in the 'mycluster' config task as +the 'mylogin' user with a single command: + +var control = require('./node-control'), + task = control.task; + +task('mycluster', 'Config for my cluster', function () { + var config, addresses; + config = { + user: 'mylogin' + }; + addresses = [ 'a.mydomain.com', + 'b.mydomain.com', + 'c.mydomain.com' ]; + return control.hosts(config, addresses); +}); + +task('date', 'Get date', function (host) { + host.ssh('date'); +}); + +control.begin(); + + +If saved in a file named 'mycontroller.js', run with: + +node mycontroller.js mycluster date + + +Each machine is contacted in parallel, date is executed, and the output from +the remote machine is printed to the console. Example output: + + Performing mycluster + Performing date for a.mydomain.com +a.mydomain.com:mylogin:ssh: date + Performing date for b.mydomain.com +a.mydomain.com:mylogin:ssh: date + Performing date for c.mydomain.com +a.mydomain.com:mylogin:ssh: date +a.mydomain.com:stdout: Sun Jul 18 13:30:50 UTC 2010 +a.mydomain.com:exit: 0 +b.mydomain.com:stdout: Sun Jul 18 13:30:50 UTC 2010 +c.mydomain.com:stdout: Sun Jul 18 13:30:50 UTC 2010 +b.mydomain.com:exit: 0 +c.mydomain.com:exit: 0 + + +Each line of output is labeled with the address of the host the command was +executed on. The actual command sent and the user used to send it is +displayed. stdout and stderr output of the remote process is identified +as well as the final exit code of the local ssh command. Each line also appears +timestamped in a hosts.log file in the current working directory. + + + +CODE DEPLOYMENT EXAMPLE + +A task that will upload a local zip file containing a release of a node +application to a remote machine, unzip it, and start the node application. + +var path = require('path'); + +task('deploy', 'Deploy my app', function (host, release) { + var basename = path.basename(release), + remoteDir = '/apps/', + remotePath = path.join(remoteDir, basename), + remoteAppDir = path.join(remoteDir, 'myapp'); + host.scp(release, remoteDir, function () { + host.ssh('tar xzvf ' + remotePath + ' -C ' + remoteDir, function () { + host.ssh("sh -c 'cd " + remoteAppDir + " && node myapp.js'"); + }); + }); +}); + +Execute as follows, for example: + +node mycontroller.js mycluster deploy ~/myapp/releases/myapp-1.0.tgz + + +A full deployment solution would deal with shutting down the existing +application and have different directory conventions. node-control does not +assume a particular style or framework, but provides tools to build a +custom deployment strategy for your application or framework. + + + +INSTALLATION + +Clone this repository with git or download the latest version using the GitHub +repository Downloads link. Then use as a standard Node module by requiring the +node-control directory. + +If you use npm: + +npm install control + + +If you install the npm package, use this require statement: + +var control = require('control'); + + + +CONFIG TASKS + +In node-control, you always call two tasks on the command line when doing +remote operations. The first task is the config task, which must return an +array of host objects. Each host object represents a single host and has its +own set of properties. The hosts() method exported by control.js allows you to +take a single object literal config and multiply it by an array of addresses to +get a list of host objects that all (prototypically) inherit from the same +config: + +task('mycluster', 'Config for my cluster', function () { + var config, addresses; + config = { + user: 'mylogin' + }; + addresses = [ 'a.mydomain.com', + 'b.mydomain.com', + 'c.mydomain.com' ]; + return control.hosts(config, addresses); +}); + + +Config tasks enable definition of reusable work tasks independent of the +machines they will control. For example, if you have a staging environment with +different machines than your production environment you can create two +different config tasks returning different hosts, yet use the same deploy task: + +node mycontroller.js stage deploy ~/myapp/releases/myapp-1.0.tgz +... +node mycontroller.js production deploy ~/myapp/releases/myapp-1.0.tgz + + + +HOST OBJECTS + +Each host object returned by the config task is independently passed to the +second task, which is the work task ('deploy' in the above example). The host +object is always passed to the work task's function as the first +argument: + +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 +machine. + +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: + + host.scp(release, remoteDir, function () { + host.ssh('tar xzvf ' + remotePath + ' -C ' + remoteDir, function () { + + +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. + + + +ARGUMENTS + +Arguments on the command line after the name of the work task become arguments +to the work task's function: + +task('deploy', 'Deploy my app', function (host, release) { + + +This command: + +node mycontroller.js stage deploy ~/myapp/releases/myapp-1.0.tgz + + +Results in release = '~/myapp/releases/myapp-1.0.tgz'. + +More than one argument is possible: + +task('deploy', 'Deploy my app', function (host, release, tag) { + + + +BEGIN + +To execute the tasks using a tasks file, use the begin() method at the +bottom of the tasks file: + +var control = require('./node-control'); +... // Define tasks +control.begin(); + +begin() calls the first (config) task identified on the command line to get the +array of host objects, then calls the second (work) task with each of the host +objects. From that point, everything happens asynchronously as all hosts work +their way through the work task. + + + +PERFORMING MULTIPLE TASKS + +A task can call other tasks using perform() and optionally pass arguments to +them: + +var perform = require('./node-control').perform; +... +task('mytask', 'My task description', function (host, argument) { + perform('anothertask', host, argument); + + +perform() requires only the task name and the host object. Arguments are +optional. If the other task supports it, optionally pass a callback function as +one of the arguments: + + perform('anothertask', host, function () { + + +Tasks that support asynchronous performance should call the callback function +when done doing their own work. For example: + +task('anothertask', 'My other task description', function (host, callback) { + host.ssh('date', function () { + if (callback) { + callback(); + } + }); +}); + + +The peform() call can occur anywhere in a task, not just at the beginning. + + + +LIST TASKS + +To list all defined tasks with descriptions: + +node mycontroller.js mycluster list + + + +NAMESPACES + +Use a colon, dash, or similar convention when naming if you want to group tasks +by name. For example: + +task('bootstrap:tools', 'Bootstrap tools', function (host) { +... +task('bootstrap:compilers', 'Bootstrap compilers', function (host) { + + + +SUDO + +To use sudo, just include sudo as part of your command: + + host.ssh('sudo date'); + + +This requires that sudo be installed on the remote machine and have requisite +permissions setup. + + + +ROLES + +Some other frameworks like Vlad and Capistrano provide the notion of roles for +different hosts. node-control does not employ a separate roles construct. Since +hosts can have any properties defined on them in a config task, a possible +pattern for roles if needed: + +task('mycluster', 'Config for my cluster', function () { + var appConfig, dbConfig, dbs, apps; + + dbConfig = { + user: 'dbuser', + role: 'db' + }; + + dbs = control.hosts(dbConfig, ['db1.mydomain.com', 'db2.mydomain.com']); + + appConfig = { + user: 'appuser', + role: 'app' + }; + apps = control.hosts(appConfig, ['app1.mydomain.com', 'app2.mydomain.com']); + + return dbs.concat(apps); +}); + +task('deploy', 'Deploy my app', function (host, release) { + if (host.role === 'db') { + // Do db deploy work + } + + if (host.role === 'app') { + // Do app deploy work + } +}); + + + +LOGS + +All commands sent and responses received are logged with timestamps (from the +control machine's clock). By default, logging goes to a hosts.log file in the +working directory of the node process. However, you can override this in your +control script: + +task('mycluster', 'Config for my cluster', function () { + var config, addresses; + config = { + user: 'mylogin', + log: '~/mycluster-control.log' + }; + addresses = [ 'myhost1.mydomain.com', + 'myhost2.mydomain.com', + 'myhost3.mydomain.com' ]; + 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: + +grep myhost1.mydomain.com hosts.log | less + + +Would allow paging the log and seeing only lines pertaining to +myhost1.mydomain.com. + + +If you send something you do not want to get logged (like a password) in a +command, use the log mask: + +host.logMask = secret; +host.ssh('echo ' + secret + ' > file.txt'); + + +The console and command log file will show the masked text as asterisks instead +of the actual text. + + + +SSH + +To avoid repeatedly entering passwords across possibly many hosts, use standard +ssh keypair authentication. + +Each host.ssh() call requires a new connection to the host. To configure ssh to +reuse a single connection, place this: + +Host * +ControlMaster auto +ControlPath ~/.ssh/master-%r@%h:%p + + +In your ssh config file (create if it does not exist): + +~/.ssh/config + + +To pass options to the ssh command when using ssh(), add the option or options +as an array to the sshOptions property of the config, or modify the sshOptions +property of the host on the fly: + + // Config task + config = { + user: 'mylogin', + sshOptions: [ '-2', '-p 44' ] + }; + + // Work task + host.sshOptions.push('-v'); + + +Use scpOptions in the same manner for 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 not necessary since you can edit the config file at any time, +but is available if config tasks need to have command line arguments or rewrite +the work task name and its arguments on the fly. + + + +QUICK EXAMPLE WITHOUT TASKS + +You can create scripts to run individually instead of through the tasks system +by using hosts() to create an array of hosts objects and using the hosts +objects directly: + +var control = require('../'), + config = { + user: 'mylogin' + }, + addresses = [ 'a.mydomain.com', + 'b.mydomain.com', + 'c.mydomain.com' ], + hosts = control.hosts(config, addresses), + i, l, host; + +for (i = 0, l = hosts.length; i < l; i += 1) { + host = hosts[i]; + host.ssh('date'); +} + +If saved in a file named 'mycontroller.js', execute with: + +node mycontroller.js + + + +FEEDBACK + +Welcome at node@thomassmith.com or the Node mailing list. diff --git a/deps/node-control/index.js b/deps/node-control/index.js new file mode 100644 index 00000000..be10caf3 --- /dev/null +++ b/deps/node-control/index.js @@ -0,0 +1,3 @@ +/*global module, require */ + +module.exports = require('./lib'); diff --git a/deps/node-control/lib/host.js b/deps/node-control/lib/host.js new file mode 100644 index 00000000..104b4d8a --- /dev/null +++ b/deps/node-control/lib/host.js @@ -0,0 +1,168 @@ +/*global require, exports, spawn: true */ + +var spawn = require('child_process').spawn, + path = require('path'), + Log = require('./log').Log; + +function logBuffer(log, prefix, buffer) { + var message = buffer.toString(); + log.puts(message, prefix); +} + +function listen(subProcess, log, callback, expect, prefix) { + var do_expect = (typeof expect != 'undefined') ? true : false; + if (typeof prefix == 'undefined') prefix = ''; + else prefix += ' '; + var codes = ''; + var buff = ''; + var stdout = ''; + var stderr = ''; + subProcess.stdout.addListener('data', function (data) { + if (do_expect) buff += data.toString(); + stdout += data.toString(); + logBuffer(log, 'stdout: ', data); + }); + + subProcess.stderr.addListener('data', function (data) { + if (do_expect) buff += data.toString(); + stderr += data.toString(); + logBuffer(log, 'stderr: ', data); + }); + + subProcess.addListener('exit', function (code) { + if (do_expect && buff.substr(0, buff.length - 1) != expect) { + console.error(prefix + 'Error: Unexpected result: ' + buff.substr(0, buff.length - 1) + ' != ' + expect); + } + logBuffer(log, 'exit: ', code); + if (code === 0) code = undefined; + else console.error(prefix + 'Error: Exit: ' + code.toString()); + if (callback) callback(code, stdout, stderr); + }); +} + +function star(mask) { + var stars = '', + i, length; + for (i = 0, length = mask.length; i < length; i += 1) { + stars += '*'; + } + return stars; +} + +function ssh(command, expect, callback) { + if (!command) { + throw new Error(this.address + ': No command to run'); + } + + var log = this.logger, + user = this.user, + options = this.sshOptions, + mask = this.logMask, stars, + args = ['-l' + user, this.address, "''" + command + "''"], + subProcess; + + if (options) { + args = options.concat(args); + } + + if (mask) { + stars = star(mask); + while (command.indexOf(mask) !== -1) { + command = command.replace(mask, stars); + } + } + + log.puts(user + ':ssh ' + command); + subProcess = spawn('ssh', args); + listen(subProcess, log, callback, expect, this.user + '@' + this.address + ':ssh ' + command); +} + +function scp(local, remote, callback, exitCallback) { + if (!local) { + throw new Error(this.address + ': No local file path'); + } + + if (!remote) { + throw new Error(this.address + ': No remote file path'); + } + + 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, exitCallback); + } else { + throw new Error('Local: ' + local + ' does not exist'); + } + }); +} + +function log(message) { + this.logger.puts(' ' + message); +} + +var defaultLogPath = 'hosts.log'; + +function hostConstructor(config) { + + // Initialize ssh and scp options to an array if not specified so later + // logic can assume an array exists when adding or removing options. + config.sshOptions = config.sshOptions || []; + config.scpOptions = config.scpOptions || []; + + // This function may get called with different config objects + // during a single config task (see roles example in README). Therefore + // we cannot define the constructor as a function declaration at module + // scope and modify its prototype because the last config would become + // the config for all hosts. + function Host(address) { + var logPath = config.log || defaultLogPath; + var echo = config.echo || false; + this.address = address; + this.logger = new Log(this.address + ':', logPath, echo); + this.log = log; + this.ssh = ssh; + this.scp = scp; + + // Allows task execution output to identify the host a task + // is being executed for. + this.id = address; + + } + Host.prototype = config; + return Host; +} + +function hosts(config, addresses) { + if (!config) { + throw new Error("No config"); + } + + if (!addresses || !(addresses instanceof Array)) { + throw new Error("No array of addresses"); + } + + var list = [], + i, length, address, host, + Host = hostConstructor(config); + for (i = 0, length = addresses.length; i < length; i += 1) { + address = addresses[i]; + host = new Host(address); + list.push(host); + } + return list; +} + +exports.hosts = hosts; diff --git a/deps/node-control/lib/index.js b/deps/node-control/lib/index.js new file mode 100644 index 00000000..a15dd160 --- /dev/null +++ b/deps/node-control/lib/index.js @@ -0,0 +1,9 @@ +/*global require, exports */ + +var task = require('./task'), + host = require('./host'); + +exports.task = task.task; +exports.begin = task.begin; +exports.perform = task.perform; +exports.hosts = host.hosts; diff --git a/deps/node-control/lib/log.js b/deps/node-control/lib/log.js new file mode 100644 index 00000000..fdbaa582 --- /dev/null +++ b/deps/node-control/lib/log.js @@ -0,0 +1,75 @@ +// Provides a grep-friendly log where every line is prefixed with +// a prefix given during construction. Output goes to file and/or +// console or nowhere. Each line of output to a file is timestamped. + +/*global require, exports */ + +var sys = require('sys'), + fs = require('fs'); + +// prefix: added to log prefix to appear on each line if message has new lines +function puts(message, prefix) { + var filestream = this.filestream, + logPrefix = this.prefix, + echo = this.echo, + timestamp = this.timestamp, + lines, line, i, length; + + if (prefix) { + logPrefix += prefix; + } + + // Message may contain leading, interstitial, or trailing carriage + // returns and new lines. Carriage returns, when outputted to the + // console (terminal) will act as a literal carriage return, going back to + // the beginning of the line and overwriting content laid down in a + // previous line, which makes the console (terminal) log look corrupted. + // The strategy here is to convert all carriage returns into new lines and + // any group of new lines into one new line and then log each line with the + // timestamp and prefix. If you decide that all this trimming and + // indenting can be done more efficiently, please keep the carriage return + // issue in mind. + message = message.replace(/\r/g, "\n"); // Carriage return conversion + message = message.replace(/^\n+|\n+$/g, ""); // Start and end clean up + message = message.replace(/\n+/g, "\n"); // Group interstitial new lines + + lines = message.split("\n"); + for (i = 0, length = lines.length; i < length; i += 1) { + line = lines[i]; + if (line.length > 0) { // Disregard empty lines + + if (filestream) { + filestream.write(timestamp.now() + ':' + + logPrefix + line + '\n'); + } + + if (echo) { + sys.puts(logPrefix + line); + } + } + } +} + +// prefix: prefix that will be prefixed to every line of output +// path: (optional) file path of persisted log +// echo: (optional) true to echo to console, false otherwise +// timestamper: (optional) object that returns a timestamp from now() method +function Log(prefix, path, echo, timestamp) { + var filestream; + + if (path) { + filestream = fs.createWriteStream(path, { flags: 'a', mode: 0600 }); + } + + timestamp = timestamp || require('./timestamp'); + + return { + prefix: prefix, + filestream: filestream, + echo: echo, + timestamp: timestamp, + puts: puts + }; +} + +exports.Log = Log; diff --git a/deps/node-control/lib/task.js b/deps/node-control/lib/task.js new file mode 100644 index 00000000..e466b6c2 --- /dev/null +++ b/deps/node-control/lib/task.js @@ -0,0 +1,91 @@ +/*global require, exports, process */ + +var sys = require('sys'); + +var tasks = {}, + descriptions = {}; + +// unshifted: arguments object from another function +// arguments object does not implement Array methods so this function provides a +// logical arguments.shift() +function shift(unshifted) { + var i, length, shifted = []; + for (i = 1, length = unshifted.length; i < length; i += 1) { + shifted[i - 1] = unshifted[i]; + } + return shifted; +} + +function perform(name, config) { + if (!name) { + throw new Error('No task name'); + } + + var task = tasks[name], + log = " Performing " + name; + + if (config && config.id) { + log += " for " + config.id; + } + + // sys.puts(log); + + if (!task) { + throw new Error('No task named: ' + name); + } + + return task.apply(null, shift(arguments)); +} + +function performAll(configs) { + if (!configs || !(configs instanceof Array) || configs.length < 1) { + throw new Error('No array of config objects'); + } + + var i, length, config, + args = shift(arguments), + argsWithConfig; + + for (i = 0, length = configs.length; i < length; i += 1) { + config = configs[i]; + + // Copy the arguments array for each config and insert the + // config object as the first argument to the perform function + // to use when calling the task. + argsWithConfig = args.slice(0); + argsWithConfig.splice(1, 0, config); + + perform.apply(null, argsWithConfig); + } +} + +function task(name, description, callback) { + tasks[name] = callback; + descriptions[name] = description; +} + +function list() { + for (var i in tasks) { + if (tasks.hasOwnProperty(i)) { + sys.puts(i + ': ' + descriptions[i]); + } + } +} + +function begin() { + var configTask = process.argv[2], + taskWithArgs = process.argv.slice(3), + configs = perform(configTask, taskWithArgs); + if (taskWithArgs.length > 0) { + taskWithArgs.unshift(configs); + performAll.apply(null, taskWithArgs); + } +} + +task('list', 'List tasks', function () { + list(); +}); + +exports.task = task; +exports.begin = begin; +exports.perform = perform; diff --git a/deps/node-control/lib/timestamp.js b/deps/node-control/lib/timestamp.js new file mode 100644 index 00000000..283bc92a --- /dev/null +++ b/deps/node-control/lib/timestamp.js @@ -0,0 +1,36 @@ +// Creates a YYYYMMDDhhmmss timestamp. Ideas on how to do this more efficiently +// are welcome. + +/*global require, exports */ + +var sys = require('sys'); + +function padLeft(message, length) { + var delta, i; + message = message.toString(); + delta = length - message.length; + for (i = 0; i < delta; i += 1) { + message = '0' + message; + } + return message; +} + +function now() { + var d = new Date(), + year = d.getFullYear(), + month = d.getMonth() + 1, + day = d.getDate(), + minute = d.getMinutes(), + hour = d.getHours(), + second = d.getSeconds(); + + year = padLeft(year, 4); + month = padLeft(month, 2); + day = padLeft(day, 2); + hour = padLeft(hour, 2); + minute = padLeft(minute, 2); + second = padLeft(second, 2); + return (year + month + day + hour + minute + second); +} + +exports.now = now; diff --git a/deps/node-control/package.json b/deps/node-control/package.json new file mode 100644 index 00000000..8c9687bb --- /dev/null +++ b/deps/node-control/package.json @@ -0,0 +1,7 @@ +{ "name" : "control", + "description" : "Scripted asynchronous control of remote machines in parallel via ssh", + "version" : "0.1.9", + "author" : "Thomas Smith ", + "main" : "./lib", + "engines" : { "node" : ">=0.1.99" } +}