341 lines
11 KiB
JavaScript
Executable File
341 lines
11 KiB
JavaScript
Executable File
#!/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 <value>', 'Cloudron IP')
|
|
.action(login);
|
|
|
|
program.command('logs')
|
|
.description('Fetch logs by filename')
|
|
.option('--ip <value>', '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 <value>', 'Cloudron IP')
|
|
.option('--code <code>', 'Code tarball')
|
|
.action(hotfix);
|
|
|
|
program.command('backup')
|
|
.description('Backup a cloudron')
|
|
.option('--ip <value>', '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);
|
|
}
|
|
}
|