Permalink
Browse files

Initial commit.

  • Loading branch information...
0 parents commit 21f7f1155783570de0dafc18978c3ead77ec17bd Tom Smith committed Jul 16, 2010
Showing with 705 additions and 0 deletions.
  1. +19 −0 LICENSE
  2. +351 −0 README
  3. +10 −0 control.js
  4. +115 −0 lib/host.js
  5. +77 −0 lib/log.js
  6. +95 −0 lib/task.js
  7. +38 −0 lib/timestamp.js
@@ -0,0 +1,19 @@
+Copyright (c) 2010 Thomas Smith <node@thomassmith.com> (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.
@@ -0,0 +1,351 @@
+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
+
+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('./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 'mycontrols.js', run with:
+
+node mycontrols.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: date
+ Performing date for b.mydomain.com
+a.mydomain.com:mylogin: date
+ Performing date for c.mydomain.com
+a.mydomain.com:mylogin: 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('cd ' + remoteAppDir + '; node myapp.js');
+ });
+ });
+});
+
+Execute as follows, for example:
+
+node mycontrols.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
+
+The node-control module consists of the control.js file and the lib
+subdirectory with submodules that make up the various parts of node-control,
+which are all just .js files. You should be able to clone the git repository or
+download the files directly and either put them in your current working
+directory or your node library and require them per usual Node require
+patterns.
+
+
+
+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 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 resuable 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 mycontrols.js stage deploy ~/myapp/releases/myapp-1.0.tgz
+...
+node mycontrols.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 filepath 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 () {
+
+
+Callbacks can be chained as far as necessary.
+
+
+
+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 mycontrols.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('./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('./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 callbck 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 mycontrols.js 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.
+
+
+
+FEEDBACK
+
+Feedback, questions, corrections, and suggestions welcome at
+node@thomassmith.com or the node.js mailing list.
@@ -0,0 +1,10 @@
+/*global require, exports */
+
+var task = require('./lib/task'),
+ host = require('./lib/host');
+
+exports.task = task.task;
+exports.begin = task.begin;
+exports.perform = task.perform;
+exports.hosts = host.hosts;
+exports.host = host.perform;
Oops, something went wrong.

0 comments on commit 21f7f11

Please sign in to comment.