#!/usr/bin/env node 'use strict'; var assert = require('assert'), async = require('async'), crypto = require('crypto'), execSync = require('child_process').execSync, fs = require('fs'), https = require('https'), os = require('os'), path = require('path'), program = require('commander'), readlineSync = require('readline-sync'), spawn = require('child_process').spawn, SshClient = require('ssh2').Client, superagent = require('superagent'), util = require('util'); require('colors'); var SSH = 'root@%s -tt -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ConnectTimeout=10 -i %s'; var sshKeyPath = path.join(process.env.HOME, '/.ssh/id_rsa_yellowtent'); if (!process.env['DIGITAL_OCEAN_TOKEN_DEV']) exit('Missing env variable DIGITAL_OCEAN_TOKEN_DEV'); if (!process.env['DIGITAL_OCEAN_TOKEN_STAGING']) exit('Missing env variable DIGITAL_OCEAN_TOKEN_STAGING'); if (!fs.existsSync(sshKeyPath)) exit('Unable to find ssh key path. Searching for ' + sshKeyPath); // Allow self signed certs! process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; function exit(error) { if (error) console.log(error); process.exit(error ? 1 : 0); } function getDroplets(token, callback) { assert.strictEqual(typeof token, 'string'); assert.strictEqual(typeof callback, 'function'); var droplets = []; var nextPage = null; async.doWhilst(function (callback) { var url = nextPage ? nextPage : 'https://api.digitalocean.com/v2/droplets'; superagent.get(url).set('Authorization', 'Bearer ' + token).end(function (error, result) { if (error) return callback(error.message); if (result.statusCode === 403) return callback('Invalid Digitalocean credentials'); if (result.statusCode !== 200) return callback(util.format('Unable to get droplet list. %s - %s', result.statusCode, result.text)); nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null; droplets = droplets.concat(result.body.droplets); callback(null); }); }, function () { return !!nextPage; }, function (error) { if (error) return callback(error); callback(null, droplets); }); } function selectCloudron(action) { assert.strictEqual(typeof action, 'function'); var dropletsDev = []; var dropletsStaging = []; var dropletsProd = []; console.log('Getting droplet lists from dev and staging...'); getDroplets(process.env['DIGITAL_OCEAN_TOKEN_DEV'], function (error, result) { if (error) exit(error); dropletsDev = result; getDroplets(process.env['DIGITAL_OCEAN_TOKEN_STAGING'], function (error, result) { if (error) exit(error); dropletsStaging = result; getDroplets(process.env['DIGITAL_OCEAN_TOKEN_PROD'], function (error, result) { if (error) exit(error); dropletsProd = result; console.log(); console.log('Available Droplets on dev:'.bold); dropletsDev.forEach(function (droplet, index) { console.log('\t(%s)\t%s %s', index, droplet.name.cyan, droplet.networks.v4[0].ip_address); }); console.log(); console.log('Available Droplets on staging:'.bold); dropletsStaging.forEach(function (droplet, index) { console.log('\t(%s)\t%s %s', dropletsDev.length + index, droplet.name.cyan, droplet.networks.v4[0].ip_address); }); console.log(); console.log('Available Droplets on prod:'.bold); dropletsProd.forEach(function (droplet, index) { console.log('\t(%s)\t%s %s', dropletsDev.length + dropletsStaging.length + index, droplet.name.cyan, droplet.networks.v4[0].ip_address); }); console.log(); var droplets = dropletsDev.concat(dropletsStaging).concat(dropletsProd); var index = -1; while (true) { index = parseInt(readlineSync.question('Choose cloudron [0-' + (droplets.length-1) + ']: ', {})); if (isNaN(index) || index < 0 || index > droplets.length-1) console.log('Invalid selection'.red); else break; } action(droplets[index].networks.v4[0].ip_address); }); }); }); } function loginToCloudron(ip) { assert.strictEqual(typeof ip, 'string'); console.log('Ssh into %s'.bold, ip.cyan); var ssh = spawn('ssh', util.format(SSH, ip, sshKeyPath).split(' ')); ssh.on('exit', exit); ssh.on('error', exit); process.stdin.setEncoding('utf8'); process.stdin.setRawMode(true); process.stdin.pipe(ssh.stdin); ssh.stdout.pipe(process.stdout); ssh.stderr.pipe(process.stderr); process.stdin.resume(); } function logsFromCloudron(ip, fileName, tail) { assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof fileName, 'string'); assert.strictEqual(typeof tail, 'boolean'); console.log('Fetching logs from'.bold, ip.cyan); var options = { hostname: ip, port: 886, path: util.format('/api/v1/installer/logs?filename=%s&tail=%s', fileName, tail), method: 'GET', key: fs.readFileSync(path.join(__dirname, '../../keys/installer/server.key')), cert: fs.readFileSync(path.join(__dirname, '../../keys/installer/server.crt')), ca: fs.readFileSync(path.join(__dirname, '../../keys/installer_ca/ca.crt')), rejectUnauthorized: false }; var req = https.request(options, function (res) { res.setEncoding('utf8'); res.on('data', function (chunk) { process.stdout.write(chunk); }); }); req.on('error', function (error) { exit(error); }); req.end(); } function triggerBackup(ip) { assert.strictEqual(typeof ip, 'string'); console.log('Trigger backup on %s'.bold, ip.cyan); var options = { hostname: ip, port: 886, path: '/api/v1/installer/backup', method: 'POST', key: fs.readFileSync(path.join(__dirname, '../../keys/installer/server.key')), cert: fs.readFileSync(path.join(__dirname, '../../keys/installer/server.crt')), ca: fs.readFileSync(path.join(__dirname, '../../keys/installer_ca/ca.crt')), rejectUnauthorized: false }; var req = https.request(options, function (res) { res.setEncoding('utf8'); res.on('data', function (chunk) { process.stdout.write(chunk); }); }); req.on('error', function (error) { exit(error); }); req.end(); } function sshExec(ip, cmds) { var privateKey = path.join(process.env.HOME, '.ssh/id_rsa_yellowtent'); if (!fs.existsSync(privateKey)) exit('cannot find private key'); var sshClient = new SshClient(); sshClient.connect({ host: ip, port: 22, username: 'root', privateKey: fs.readFileSync(privateKey) }); sshClient.on('ready', function () { console.log('connected'); async.eachSeries(cmds, function (cmd, iteratorDone) { console.log(cmd.cmd.yellow); sshClient.exec(cmd.cmd, function(err, stream) { if (err) exit(err.message); if (cmd.stdin) cmd.stdin.pipe(stream); stream.pipe(process.stdout); stream.on('close', function () { iteratorDone(); }); }); }, function seriesDone(error) { if (error) exit(error.message); console.log('Done patching'.green); sshClient.end(); }); }); sshClient.on('error', function (error) { exit(error.message); }); sshClient.on('exit', function (exitCode) { console.log('exit'); process.exit(exitCode); }); } function hotfixCloudron(ip, code) { var CMDS = [ { cmd: 'supervisorctl stop all' }, { cmd: 'rm -rf /home/yellowtent/box/* /home/yellowtent/box/.*' }, { cmd: 'tar zxf - -C /home/yellowtent/box', stdin: fs.createReadStream(code) }, { cmd: 'cd /home/yellowtent/box && npm rebuild' }, { cmd: 'chown -R yellowtent.yellowtent /home/yellowtent/box' }, { cmd: 'sed -e "s/restoreUrl/_restoreUrl/" -i /home/yellowtent/setup_start.sh' }, // do not restore { cmd: '/home/yellowtent/setup_start.sh' } // ensure db-migrate runs as well ]; sshExec(ip, CMDS); } function hotfix(options) { var code; if (!options.code) { var answer = readlineSync.question('Create a tarball from repo (y/n)? '); if (answer !== 'y') return exit(); code = os.tmpdir() + '/boxtarball.tar.gz'; execSync(path.join(__dirname, '../images/createBoxTarball --output ' + code + ' --no-upload'), { stdio: [ null, process.stdout, process.stderr ] }); } else { code = options.code; } if (!options.ip) { selectCloudron(function (ip) { hotfixCloudron(ip, code); }); } else { hotfixCloudron(options.ip, code); } } function login(options) { if (!options.ip) selectCloudron(loginToCloudron); else loginToCloudron(options.ip); } function logs(options) { var fileName = '/var/log/supervisor/box.log'; if (options.installer) fileName = '/var/log/cloudron/installserver.log'; if (options.nginxAccess) fileName = '/var/log/nginx/access.log'; if (options.nginxError) fileName = '/var/log/nginx/error.log'; if (!options.ip) selectCloudron(function (ip) { logsFromCloudron(ip, fileName, !!options.tail); }); else logsFromCloudron(options.ip, fileName, !!options.tail); } function backup(options) { if (!options.ip) selectCloudron(triggerBackup); else triggerBackup(options.ip); } // entry point program.version('0.1.0'); program.command('login') .description('Login to cloudron') .option('--ip ', 'Cloudron IP') .action(login); program.command('logs') .description('Fetch logs by filename') .option('--ip ', 'Cloudron IP') .option('-f, --tail', 'tail the logs') .option('--installer', 'installer logs') .option('--nginx-error', 'nginx error logs') .option('--nginx-access', 'nginx access logs') .option('--box', 'box logs [default]') .action(logs); program.command('hotfix') .description('Hotfix a cloudron') .option('--ip ', 'Cloudron IP') .option('--code ', 'Code tarball') .action(hotfix); program.command('backup') .description('Backup a cloudron') .option('--ip ', 'Cloudron IP') .action(backup); program.parse(process.argv); if (!process.argv.slice(2).length) { program.outputHelp(); } else { // https://github.com/tj/commander.js/issues/338 var knownCommand = program.commands.some(function (command) { return command._name === process.argv[2]; }); if (!knownCommand) { console.error('Unknown command: ' + process.argv[2]); process.exit(1); } }