tooltwist
Version:
Tooltwist Command Line Interface
521 lines (447 loc) • 14.9 kB
JavaScript
/**
* Create .fip-server on the local machine and .fip-destination
* on a remote server using SSH, so that FIP can transfer files
* between the two machines.
*/
var Connection = require('ssh2'),
Config = require('./config'),
Log = require('./log'),
mkdirp = require('mkdirp'),
fs = require('fs');
var FIP_SOURCE_PATH = null;
var FIP_DESTINATION_PATH = null;
var DEFAULT_DESTINATION_EXTRA = ""
+ "protect=mysql/.*,tomcat/work/.*,tomcat/logs/.*,protected/.*,tomcat/conf/tomcat-users.xml\n"
+ "preCommitCommand=protected/pre_commit.sh\n"
+ "postCommitCommand=protected/post_commit.sh\n";
/**
* Read the source keys file
*/
function readFipSource(callback){
fs.readFile(FIP_SOURCE_PATH, {encoding:'utf8'}, function(err, data){
if (err) {
Log.debug('Err is ', err)
if (err.code == 'ENOENT') {
// No definition file
Log.debug("MISSING .fip-source");
return callback(null, null); // no file
}
return callback(err, null);
}
Log.debug('file is '+ data)
var obj = {
sourceUuid : null,
destinations : [ ]
};
var lines = data.split("\n")
Log.debug('\nlines: ', lines)
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
var s = 'sourceUuid='
var d = 'destinationUuid='
var p = 'passphrase='
if (line.indexOf(s) == 0) {
obj.sourceUuid = line.substring(s.length);
continue;
} else if (line.substring(1, 1) == '#') {
continue; // comment line
}
var pos = line.indexOf('=')
if (pos > 0){
var destinationUuid = line.substring(0, pos);
var passphrase = line.substring(pos + 1)
obj.destinations[destinationUuid] = passphrase;
}
}
Log.debug('obj=', obj)
callback(null, obj);
});
}
function writeFipSource(source, callback){
Log.debug('writeFipSource()', source)
var content = '';
content += '# Configuration to make this directory a source for FIP (File Installation Protocol)\n'
content += 'sourceUuid=' + source.sourceUuid + '\n';
for (var destinationUuid in source.destinations) {
var passphrase = source.destinations[destinationUuid]
content += destinationUuid + '=' + passphrase + '\n';
}
Log.debug('SAVE:\n' + content)
fs.writeFile(FIP_SOURCE_PATH, content, { encoding: 'utf8', mode: 0600 }, function(){
callback(null);
})
}
/**
* Read the .fipDestination file from the remote server.
*/
function readFipDestination(host, sshUser, sshPort, callback) {
Log.debug('readFipDestination()')
/*
* Open the SSH connection, and an SFTP connection within it.
*/
var conn = new Connection();
conn.on('ready', function() {
Log.debug('Connection :: ready');
// Open an SFTP connection
conn.sftp(function(err, sftp) {
if (err) throw err;
Log.debug( "- SFTP started" );
// Open the file
var opts =
{
encoding: 'utf8',
autoClose: true
}
var readStream = sftp.createReadStream(FIP_DESTINATION_PATH);
var result = ''
// Handle events
readStream.on('error', function(err) {
if (err && err == 'Error: No such file') {
// Missing file.
Log.debug("MISSING FILE")
conn.end();
return callback(null, null);
}
Log.error( 'Error reading ' + FIP_DESTINATION_PATH + ':', err);
conn.end();
callback(err);
}).on('data', function(data) {
Log.debug( "readStream read" + data );
result += data
}).on('close', function() {
Log.debug( "readStream closed:\n" + result);
var lines = result.split("\n")
Log.debug('\nlines: ', lines)
var obj = { sourceUuid: null, destinationUuid: null, passphrase: null, extra: '' };
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
if (i == 0 && line.indexOf('# Configuration') == 0) continue; // Skip first line comment
var SRC_PROPERTY = 'sourceUuid='
var DST_PROPERTY = 'destinationUuid='
var PASSPHRASE_PROPERTY = 'passphrase='
if (line.indexOf(SRC_PROPERTY) == 0) {
obj.sourceUuid = line.substring(SRC_PROPERTY.length);
} else if (line.indexOf(DST_PROPERTY) == 0) {
obj.destinationUuid = line.substring(DST_PROPERTY.length);
} else if (line.indexOf(PASSPHRASE_PROPERTY) == 0) {
obj.passphrase = line.substring(PASSPHRASE_PROPERTY.length);
} else {
obj.extra += line;
}
}
// Return the destination key definition.
conn.end();
callback(null, obj);
});
}); // conn.sftp
}); // conn ready
conn.on('sftp error', function(err){
Log.error('Error: ', err)
conn.end();
callback(err, null);
});
conn.on('close', function(){
Log.debug('connection closed')
});
var options = {
host: host,
port: sshPort,
username: sshUser,
privateKey: require('fs').readFileSync(getUserHome() + '/.ssh/id_rsa'),
// passphrase: 'a stitch in time',
};
console.log('options: ', options)
conn.connect(options);
}
function getUserHome() {
return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
}
function writeFipDestination(host, sshUser, sshPort, definition, callback) {
Log.debug('writeFipDestination()', definition)
/*
* Open the SSH connection, and an SFTP connection within it.
*/
var conn = new Connection();
conn.on('ready', function() {
Log.debug('Connection :: ready');
// Open an SFTP connection
conn.sftp(function(err, sftp) {
if (err) throw err;
Log.debug( "- SFTP started" );
return writeFipDestination_part1_writeTheFile();
/**
* Install the .fipDestination file on the remote server.
*/
function writeFipDestination_part1_writeTheFile() {
Log.debug('writeFipDestination_part1_writeTheFile()')
// upload file
var opts = {
flags: 'w',
mode: 0600,
autoClose: true
};
var writeStream = sftp.createWriteStream(FIP_DESTINATION_PATH, opts);
// Handle events
writeStream.on('error', function(err) {
Log.error('Error writing ' + FIP_DESTINATION_PATH + ':' + err);
conn.end();
callback(-1)
});
writeStream.on('close', function() {
Log.debug( "sftp connection closed" );
// Change the owner
// return writeFipDestination_part2_changeItsOwner();
// Don't change the owner
conn.end();
return callback(0);
});
writeStream.on('end', function() {
Log.debug( "sftp connection end" );
});
// Work out what to put in the file
var content = '# Configuration to make this directory a destination for FIP (File Installation Protocol)\n';
content += 'sourceUuid=' + definition.sourceUuid + '\n';
content += 'destinationUuid=' + definition.destinationUuid + '\n';
content += 'passphrase=' + definition.passphrase + '\n';
content += definition.extra;
// Write the file
Log.debug('SAVE:\n' + content)
writeStream.write(content);
writeStream.end();
}
/**
* Set the owner of the .fipDestination file on the remote server.
*/
function writeFipDestination_part2_changeItsOwner() {
Log.debug('writeFipDestination_part2_changeItsOwner()');
// Run chown, to set the user and group to 'tooltwist'
var cmd = 'chown tooltwist:tooltwist ' + FIP_DESTINATION_PATH;
conn.exec(cmd, function(err, stream) {
if (err) throw err;
stream.on('exit', function(code, signal) {
Log.debug('Stream :: exit :: code: ' + code + ', signal: ' + signal);
conn.end(code);
callback(code)
}).on('close', function() {
Log.debug('Stream :: close');
}).on('data', function(data) {
Log.debug('STDOUT: ' + data);
}).stderr.on('data', function(data) {
Log.debug('STDERR: ' + data);
});
});
}
}); // conn.sftp
}); // conn ready
conn.on('error', function(err){
Log.debug('Error: ', err)
callback(err);
});
conn.on('close', function(){
Log.debug('connection closed')
});
conn.connect({
host: host,
port: sshPort,
username: sshUser,
privateKey: require('fs').readFileSync(getUserHome() + '/.ssh/id_rsa')
});
}
function newUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {var r = Math.random()*36|0,v=c=='x'?r:r&0x3|0x8;return v.toString(36);});
}
/**
*
* Pair the current machine with a remote server.
*
* There are three acceptable circumstances:
* a) No fip files on source or destination. [case 1]
* b) Fip file on source but not destination (adding an extra server to source). [case 2]
* c) Fip files on source and destination and they match. [case 4]
*
* Anything else is an error.
*/
exports.pairWithServer = function(sourceDirectory, host, sshUser, sshPort, destinationDirectory, finalCallback) {
Log.debug('pairWithServer()')
// Check the image directory exists, so we can save fip-source
mkdirp.sync(sourceDirectory);
FIP_SOURCE_PATH = sourceDirectory + '/.fip-source';
FIP_DESTINATION_PATH = destinationDirectory + '/.fip-destination';
// Get the existing definitions first
readFipSource(function(err, srcDef){
if (err) {
return finalCallback(err);
}
Log.debug('SOURCE IS:\n', srcDef);
readFipDestination(host, sshUser, sshPort, function(err, dstDef){
if (err) {
return finalCallback(err);
}
Log.debug('DESTINATION IS:\n', dstDef);
if (dstDef == null) {
if (srcDef == null) {
/*
* Case 1: New source and new destination.
*
* This is the expected case - we can pair them now.
*/
Log.debug('case 1 - NO .fip-source found locally, and NO .fip-destination on remote.')
Log.log()
Log.log('\tPairing servers over SSH.')
Log.log()
// New .fip-destination
dstDef = {
sourceUuid : newUUID(),
destinationUuid : newUUID(),
passphrase : newUUID(),
extra : DEFAULT_DESTINATION_EXTRA
};
// New .fip-source
srcDef = {
sourceUuid : dstDef.sourceUuid,
destinations: [ ]
};
srcDef.destinations[dstDef.destinationUuid] = dstDef.passphrase;
} else { // srcDef != null
/*
* Case 2: Source already exists, but not destination.
* This is okay, as a source may point to multiple destinations.
*/
Log.debug('case 2 - .fip-source IS found locally, but .fip-destination is NOT on remote.')
Log.log()
Log.log('\tThe .fip-source file already exists. This new remote server will be added.')
Log.log()
Log.log()
// Create a new .fip-destination
dstDef = {
sourceUuid : srcDef.sourceUuid,
destinationUuid : newUUID(),
passphrase : newUUID(),
extra : DEFAULT_DESTINATION_EXTRA
};
// Add the new destination to existing .fip-source
srcDef.destinations[dstDef.destinationUuid] = dstDef.passphrase;
}
//return finalCallback(null);
// Save the source and destination files.
writeFipSource(srcDef, function(err){
if (err) return finalCallback(err);
writeFipDestination(host, sshUser, sshPort, dstDef, function(errcode){
if (errcode !== 0) return finalCallback(new Error('Could not write remote ' + FIP_DESTINATION_PATH));
return finalCallback(null, true);
});
})
} else { // dstDef != null
// Destination already has keys
var badPairingError = false;
if (srcDef == null) {
/*
* Case 3: No .fip-source, but have .fip-destination.
*
* NOTE: WE MUST NEVER OVERWRITE THE SOURCE IN A DESTINATION FIP
* DEFINITION, BECAUSE THIS MIGHT CAUSE ACCIDENTALLY HIJACKING
* OF A PRODUCTION SERVER, BEING INSTALLED FROM SOMEWHERE ELSE.
*/
Log.debug('case 3 - NO .fip-source found locally, but .fip-destination IS on remote.')
Log.log('')
badPairingError = true;
} else { // srcDef != null
/*
* Case 4: have both .fip-source and .fip-destination
* If they don't match we have a problem.
*/
if (
srcDef.sourceUuid === dstDef.sourceUuid
&&
srcDef.destinations[dstDef.destinationUuid] === dstDef.passphrase
){
/*
* Case 4: have both .fip-source and .fip-destination, and they DO match.
*
* Already paired - nothing needs to be done.
*/
Log.debug('case 4 - local .fip-source and remote .fip-destination MATCH.')
Log.log('')
Log.log('The source and destination are already paired.')
return finalCallback(null, true);
} else {
/*
* Case 5: have both .fip-source and .fip-destination, but they don't match.
*
* THE DESTINATION IS MAPPED TO SOME OTHER SOURCE. THIS IS A SERIOUS ERROR.
*/
Log.debug('case 5 - local .fip-source and remote .fip-destination DO NOT match.')
Log.log('')
badPairingError = true;
}
}
if (badPairingError) {
Log.error()
Log.error('ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR')
Log.error()
Log.error()
Log.error(' The destination server already has credentials allowing installation')
Log.error(' from another source. If you are absolutely sure you want to deploy')
Log.error(' to this server from here, then remove the definition credentials from')
Log.error(' the remote server. (' + FIP_DESTINATION_PATH + ')')
Log.error()
Log.error()
Log.error('ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR')
Log.error()
Log.error()
finalCallback(null, false);
}
}
}); // readFipDestination
}); // readFipSource
}
/**
* Handle the command `tooltwist pair-fip <host> <port>`.
*
* This function does not return.
*/
exports.fromCommandLine = function(args) {
// Check the command line parameters
if (args.length < 2 || args.length > 5) usage();
// Check for the optional username
var sshUser = 'tooltwist'
if (args.length >= 3) {
sshUser = args[2]
}
// Check for optional port as an integer
var sshPort = 22;
if (args.length >= 4) {
try {
sshPort = parseInt(args[3]);
} catch (e) {
Log.error('Error: Port must be an integer.');
usage();
}
}
// Check for an optional non-standard destination directory
var destinationDirectory = '/home/' + sshUser + '/server';
if (args.length == 5) {
destinationDirectory = args[4];
}
// Do the pairing
var sourceDirectory = Config.HIDDEN_DIR+'/image';
var sshHost = args[1];
exports.pairWithServer(sourceDirectory, sshHost, sshUser, sshPort, destinationDirectory, function(err, paired){
if (err) {
Log.error('Error: not paired: ' + err);
process.exit(1);
} else if (paired) {
Log.log('Paired.')
process.exit(0);
} else {
Log.error('Not paired.')
process.exit(1);
}
});
}
function usage() {
Log.error()
Log.error('usage: tooltwist fip-pairing <hostname> [[[<sshUser>] <sshPort>] <destinationDirectory>]');
Log.error()
process.exit(1);
}