UNPKG

tooltwist

Version:
521 lines (447 loc) 14.9 kB
/** * 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); }