Flightplan

Run sequences of shell commands against local and remote hosts.
Flightplan is a node.js library for streamlining application deployment or systems administration tasks.
A complete list of changes can be found in the Changelog.
Looking for help / maintainers: See #162.
Installation & Usage
$ npm install -g flightplan
$ npm install flightplan --save-dev
$ fly [task:]<target> [--flightplan flightplan.(js|coffee)]
By default, the fly
command will try to load flightplan.js
or flightplan.coffee
.
Sample flightplan.js
var plan = require('flightplan');
plan.target('staging', {
host: 'staging.example.com',
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
});
plan.target('production', [
{
host: 'www1.example.com',
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
},
{
host: 'www2.example.com',
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
}
]);
var tmpDir = 'example-com-' + new Date().getTime();
plan.local(function(local) {
local.log('Run build');
local.exec('gulp build');
local.log('Copy files to remote hosts');
var filesToCopy = local.exec('git ls-files', {silent: true});
local.transfer(filesToCopy, '/tmp/' + tmpDir);
});
plan.remote(function(remote) {
remote.log('Move folder to web root');
remote.sudo('cp -R /tmp/' + tmpDir + ' ~', {user: 'www'});
remote.rm('-rf /tmp/' + tmpDir);
remote.log('Install dependencies');
remote.sudo('npm --production --prefix ~/' + tmpDir
+ ' install ~/' + tmpDir, {user: 'www'});
remote.log('Reload application');
remote.sudo('ln -snf ~/' + tmpDir + ' ~/example-com', {user: 'www'});
remote.sudo('pm2 reload example-com', {user: 'www'});
});
plan.local(function(local) { });
plan.remote(function(remote) { });
Documentation
Flightplan
A flightplan is a set of subsequent flights to be executed on one or more
hosts. Configuration is handled with the target()
method.
var plan = require('flightplan');
Flights
A flight is a set of commands to be executed on one or more hosts. There are
two types of flights:
Local flights
Commands in local flights are executed on the localhost.
plan.local(function(transport) {
transport.hostname();
});
Remote flights
Commands in remote flights are executed in parallel against remote hosts.
plan.remote(function(transport) {
transport.hostname();
});
You can define multiple flights of each type. They will be executed in the
order of their definition. If a previous flight failed, all subsequent
flights won't get executed. For more information about what it means for
a flight to fail, see the section about Transport
.
plan.local(function(transport) {});
plan.remote(function(transport) {});
plan.local(function(transport) {});
Tasks
Flightplan supports optional tasks to run a subset of flights.
plan.local('deploy', function(transport) {});
plan.local('build', function(transport) {});
plan.local(['deploy', 'build'], function(transport) {});
plan.remote(['deploy', 'build'], function(transport) {});
If no task is specified it's implicitly set to "default". Therefore,
fly <target>
is the same as fly default:<target>
.
plan.local(function(transport) {});
plan.local('default', function(transport) {});
plan.remote(['default', 'deploy', 'build'], function(transport) {});
flightplan.target(name, hosts[, options]) → this
Configure the flightplan's targets with target()
. Without a
proper setup you can't do remote flights which require at
least one remote host. Each target consists of one or more hosts.
Values in the hosts section are passed directly to the connect()
method of mscdex/ssh2
with one exception: privateKey
needs to be passed as a string
containing the path to the keyfile instead of the key itself.
plan.target('staging', {
host: 'staging.example.com',
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
});
plan.target('production', [
{
host: 'www1.example.com',
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
},
{
host: 'www2.example.com',
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
}
]);
plan.target('dynamic-hosts', function(done, runtime) {
var AWS = require('aws-sdk');
AWS.config.update({accessKeyId: '...', secretAccessKey: '...'});
var ec2 = new AWS.EC2();
var params = {Filters: [{Name: 'instance-state-name', Values: ['running']}]};
ec2.describeInstances(params, function(err, response) {
if(err) {
return done(err);
}
var hosts = [];
response.data.Reservations.forEach(function(reservation) {
reservation.Instances.forEach(function(instance) {
hosts.push({
host: instance.PublicIpAddress,
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
});
});
});
done(hosts);
});
});
Usually flightplan will abort when a host is not reachable or authentication
fails. This can be prevented by setting a property failsafe
to true
on
any of the host configurations:
plan.target('production', [
{
host: 'www1.example.com',
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
},
{
host: 'www2.example.com',
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK,
failsafe: true
}
]);
You can override the username
value of hosts by calling fly
with
the -u|--username
option:
fly production --username=admin
Configuring remote hosts during runtime (e.g. using AWS/EC2)
Instead of having a static hosts configuration for a target you can configure
it on the fly by passing a function fn(done)
as the second argument to
target()
.
This function is executed at the very beginning. Whatever is passed to
done()
will be used for connecting to remote hosts. This can either be an
object or an array of objects depending on if you want to connect to one or
multiple hosts. Passing an Error
object will immediately abort the current
flightplan.
plan.target('dynamic-hosts', function(done, runtime) {
var AWS = require('aws-sdk');
AWS.config.update({accessKeyId: '...', secretAccessKey: '...'});
var ec2 = new AWS.EC2();
var params = {Filters: [{Name: 'instance-state-name', Values: ['running']}]};
ec2.describeInstances(params, function(err, response) {
if(err) {
return done(err);
}
var hosts = [];
response.data.Reservations.forEach(function(reservation) {
reservation.Instances.forEach(function(instance) {
hosts.push({
host: instance.PublicIpAddress,
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
});
});
});
done(hosts);
});
});
Defining and using properties depending on the target
target()
takes an optional third argument to define properties used by
this target. Values defined in this way can be accessed during runtime.
plan.target('staging', {...}, {
webRoot: '/usr/local/www',
sudoUser: 'www'
});
plan.target('production', {...}, {
webRoot: '/home/node',
sudoUser: 'node'
});
plan.remote(function(remote) {
var webRoot = plan.runtime.options.webRoot;
var sudoUser = plan.runtime.options.sudoUser;
remote.sudo('ls -al ' + webRoot, {user: sudoUser});
});
Properties can be set and overwritten by passing them as named options to the
fly
command.
$ fly staging --sudoUser=foo
flightplan.local([tasks, ]fn) → this
Calling this method registers a local flight. Local flights are
executed on your localhost. When fn
gets called a Transport
object
is passed with the first argument.
plan.local(function(local) {
local.echo('hello from your localhost.');
});
An optional first parameter of type Array or String can be passed for
defining the flight's task(s).
flightplan.remote([tasks, ]fn) → this
Register a remote flight. Remote flights are executed on the current
target's remote hosts defined with target()
. When fn
gets called
a Transport
object is passed with the first argument.
plan.remote(function(remote) {
remote.echo('hello from the remote host.');
});
An optional first parameter of type Array or String can be passed for
defining the flight's task(s).
flightplan.abort([message])
Manually abort the current flightplan and prevent any further commands and
flights from being executed. An optional message can be passed which
is displayed after the flight has been aborted.
plan.abort('Severe turbulences over the atlantic ocean!');
Transport
A transport is the interface you use during flights. Basically they
offer you a set of methods to execute a chain of commands. Depending on the
type of flight, this is either a Shell
object for local
flights, or an SSH
for remote flights. Both transports
expose the same set of methods as described in this section.
plan.local(function(local) {
local.echo('Shell.echo() called');
});
plan.remote(function(remote) {
remote.echo('SSH.echo() called');
});
We call the Transport object transport
in the following section to avoid
confusion. However, do yourself a favor and use local
for local, and
remote
for remote flights.
Accessing runtime information
Flightplan provides information during flights with the runtime
properties:
plan.remote(function(transport) {
console.log(plan.runtime.task);
console.log(plan.runtime.target);
console.log(plan.runtime.hosts);
console.log(plan.runtime.options);
console.log(transport.runtime);
});
transport.exec(command[, options]) → code: int, stdout: String, stderr: String
To execute a command you have the choice between using exec()
or one
of the handy wrappers for often used commands:
transport.exec('ls -al')
is the same as transport.ls('-al')
. If a
command returns a non-zero exit code, the flightplan will be aborted and
all subsequent commands and flights won't get executed.
Options
Options can be passed as a second argument. If failsafe: true
is
passed, the command is allowed to fail (i.e. exiting with a non-zero
exit code), whereas silent: true
will simply suppress its output.
transport.ls('-al', {silent: true});
transport.ls('-al foo', {failsafe: true});
transport.ls('-al foo', {silent: true, failsafe: true});
To apply these options to multiple commands check out the docs of
transport.silent()
and transport.failsafe()
.
Return value
Each command returns an object containing code
, stdout
andstderr
:
var result = transport.echo('Hello world');
console.log(result);
Advanced options
Flightplan uses child_process#exec()
for executing local commands and
mscdex/ssh2#exec()
for remote commands. Options passed with exec
will
be forwarded to either of these functions.
local.ls('-al', {exec: {maxBuffer: 2000*1024}});
remote.ls('-al', {exec: {pty: true}});
transport.sudo(command[, options]) → code: int, stdout: String, stderr: String
Execute a command as another user with sudo()
. It has the same
signature as exec()
. Per default, the user under which the command
will be executed is "root". This can be changed by passing
user: "name"
with the second argument:
transport.sudo('echo Hello world');
transport.sudo('echo Hello world', {user: 'www'});
transport.sudo('echo Hello world', {user: 'www', silent: true, failsafe: true});
Flightplan's sudo()
requires a certain setup on your host. In order to
make things work on a typical Ubuntu installation, follow these rules:
$ groups pstadler
pstadler : pstadler sudo
pstadler ALL=(www) NOPASSWD: ALL
$ cat /etc/passwd | grep www
www:x:1002:1002::/home/www:/bin/bash
www:x:1002:1002::/home/www:/bin/false
transport.transfer(files, remoteDir[, options]) → [results]
Copy a list of files to the current target's remote host(s) using
rsync
with the SSH protocol. File transfers are executed in parallel.
After finishing all transfers, an array containing results from
transport.exec()
is returned. This method is only available on local
flights.
var files = ['path/to/file1', 'path/to/file2'];
local.transfer(files, '/tmp/foo');
Files argument
To make things more comfortable, the files
argument doesn't have to be
passed as an array. Results from previous commands and zero-terminated
strings are handled as well:
var files = local.git('ls-files', {silent: true});
local.transfer(files, '/tmp/foo');
var files = local.exec('(git ls-files -z;find node_modules -type f -print0)', {silent: true});
local.transfer(files, '/tmp/foo');
var result1 = local.git('ls-files', {silent: true}).stdout.split('\n');
var result2 = local.find('node_modules -type f', {silent: true}).stdout.split('\n');
var files = result1.concat(result2);
files.push('path/to/another/file');
local.transfer(files, '/tmp/foo');
transfer()
will use the current host's username defined with
target()
unless fly
is called with the -u|--username
option.
In this case the latter will be used. If debugging is enabled
(either with target()
or with fly --debug
), rsync
is executed
in verbose mode (-vv
).
transport.prompt(message[, options]) → input
Prompt for user input.
var input = transport.prompt('Are you sure you want to continue? [yes]');
if(input.indexOf('yes') === -1) {
plan.abort('User canceled flight');
}
var password = transport.prompt('Enter your password:', { hidden: true });
if(plan.runtime.target === 'production') {
var input = transport.prompt('Ready for deploying to production? [yes]');
if(input.indexOf('yes') === -1) {
plan.abort('User canceled flight');
}
}
transport.waitFor(fn(done)) → {} mixed
Execute a function and return after the callback done
is called.
This is used for running asynchronous functions in a synchronous way.
The callback takes an optional argument which is then returned by
waitFor()
.
var result = transport.waitFor(function(done) {
require('node-notifier').notify({
message: 'Hello World'
}, function(err, response) {
done(err || 'sent!');
});
});
console.log(result);
transport.with(command|options[, options], fn)
Execute commands with a certain context.
transport.with('cd /tmp', function() {
transport.ls('-al');
});
transport.with({silent: true, failsafe: true}, function() {
transport.ls('-al');
});
transport.with('cd /tmp', {silent: true}, function() {
transport.ls('-al');
});
transport.silent()
When calling silent()
all subsequent commands are executed without
printing their output to stdout until verbose()
is called.
transport.ls();
transport.silent();
transport.ls();
transport.verbose()
Calling verbose()
reverts the behavior introduced with silent()
.
Output of commands will be printed to stdout.
transport.silent();
transport.ls();
transport.verbose();
transport.ls();
transport.failsafe()
When calling failsafe()
, all subsequent commands are allowed to fail
until unsafe()
is called. In other words, the flight will continue
even if the return code of the command is not 0
. This is helpful if
either you expect a command to fail or their nature is to return a
non-zero exit code.
transport.failsafe();
transport.ls('foo');
transport.log('Previous command failed, but flight was not aborted');
transport.unsafe()
Calling unsafe()
reverts the behavior introduced with failsafe()
.
The flight will be aborted if a subsequent command fails (i.e. returns
a non-zero exit code). This is the default behavior.
transport.failsafe();
transport.ls('foo');
transport.log('Previous command failed, but flight was not aborted');
transport.unsafe();
transport.ls('foo');
transport.log(message)
Print a message to stdout. Flightplan takes care that the message
is formatted correctly within the current context.
transport.log('Copying files to remote hosts');
transport.debug(message)
Print a debug message to stdout if debug mode is enabled. Flightplan
takes care that the message is formatted correctly within the current
context.
transport.debug('Copying files to remote hosts');