[![NPM version][npm-version-image]][npm-url] [![Build Status][build-status-image]][build-status-url] [![Build Status][coverage-image]][coverage-url] [![NPM downloads][npm-downloads-image]][npm-url] [![Dependency Status][dependencies-image]][dependencies-url]
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.
# install the cli tool
$ npm install -g flightplan
# use it in your project
$ npm install flightplan --save-dev
# run a flightplan (`fly --help` for more information)
$ fly [task:]<target> [--flightplan flightplan.(js|coffee)]
By default, the fly command will try to load flightplan.js or flightplan.coffee.
// flightplan.js
var plan = require('flightplan');
// configuration
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();
// run commands on localhost
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});
// rsync files to all the target's remote hosts
local.transfer(filesToCopy, '/tmp/' + tmpDir);
});
// run commands on the target's remote hosts
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'});
});
// run more commands on localhost afterwards
plan.local(function(local) { /* ... */ });
// ...or on remote hosts
plan.remote(function(remote) { /* ... */ });
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');
A flight is a set of commands to be executed on one or more hosts. There are two types of flights:
Commands in local flights are executed on the localhost.
plan.local(function(transport) {
transport.hostname(); // prints the hostname of localhost
});
Commands in remote flights are executed in parallel against remote hosts.
plan.remote(function(transport) {
transport.hostname(); // prints the hostname(s) of the remote host(s)
});
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.
// executed first
plan.local(function(transport) {});
// executed if first flight succeeded
plan.remote(function(transport) {});
// executed if second flight succeeded
plan.local(function(transport) {});
// ...
Flightplan supports optional tasks to run a subset of flights.
// fly deploy:<target>
plan.local('deploy', function(transport) {});
// fly build:<target>
plan.local('build', function(transport) {});
// fly deploy:<target> or...
// fly build:<target>
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>.
// fly <target>
plan.local(function(transport) {});
// is the same as...
plan.local('default', function(transport) {});
// "default" + other tasks:
plan.remote(['default', 'deploy', 'build'], function(transport) {});
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.
// run with `fly staging`
plan.target('staging', {
// see: https://github.com/mscdex/ssh2#connection-methods
host: 'staging.example.com',
username: 'pstadler',
agent: process.env.SSH_AUTH_SOCK
});
// run with `fly production`
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
}
]);
// run with `fly dynamic-hosts`
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 // continue flightplan even if connection to www2 fails
}
]);
You can override the username value of hosts by calling fly with
the -u|--username option:
fly production --username=admin
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 exectued 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);
});
});
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; // fly staging -> '/usr/local/www'
var sudoUser = plan.runtime.options.sudoUser; // fly staging -> 'www'
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
# plan.runtime.options.sudoUser -> 'foo'
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).
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).
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!');
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.
Flightplan provides information during flights with the runtime properties:
plan.remote(function(transport) { // applies to local flights as well
// Flightplan specific information
console.log(plan.runtime.task); // 'default'
console.log(plan.runtime.target); // 'production'
console.log(plan.runtime.hosts); // [{ host: 'www1.example.com', port: 22 }, ...]
console.log(plan.runtime.options); // { debug: true, ... }
// Flight specific information
console.log(transport.runtime); // { host: 'www1.example.com', port: 22 }
});
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 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.
// output of `ls -al` is suppressed
transport.ls('-al', {silent: true});
// flightplan continues even if command fails with exit code `1`
transport.ls('-al foo', {failsafe: true}); // ls: foo: No such file or directory
// both options together
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().
Each command returns an object containing code, stdout andstderr:
var result = transport.echo('Hello world');
console.log(result); // { code: 0, stdout: 'Hello world\n', stderr: null }
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.
// increase maxBuffer for child_process#exec()
local.ls('-al', {exec: {maxBuffer: 2000*1024}});
// enable pty for mscdex/ssh2#exec()
remote.ls('-al', {exec: {pty: true}});
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:
// will run: echo 'echo Hello world' | sudo -u root -i bash
transport.sudo('echo Hello world');
// will run echo 'echo Hello world' | sudo -u www -i bash
transport.sudo('echo Hello world', {user: 'www'});
// further options passed (see `exec()`)
transport.sudo('echo Hello world', {user: 'www', silent: true, failsafe: true});
Flightplan's sudo() re
$ claude mcp add flightplan \
-- python -m otcore.mcp_server <graph>