Files
cloudron-box/src/cloudron.js

941 lines
34 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
CloudronError: CloudronError,
initialize: initialize,
uninitialize: uninitialize,
activate: activate,
getConfig: getConfig,
getStatus: getStatus,
2017-01-12 11:00:46 -08:00
dnsSetup: dnsSetup,
sendHeartbeat: sendHeartbeat,
2016-11-15 15:24:40 +01:00
sendAliveStatus: sendAliveStatus,
updateToLatest: updateToLatest,
reboot: reboot,
2016-01-25 20:18:50 -08:00
retire: retire,
2016-06-27 22:24:30 -05:00
migrate: migrate,
2015-10-27 16:00:31 -07:00
2017-01-05 19:01:44 -08:00
getConfigStateSync: getConfigStateSync,
2015-10-27 16:11:33 -07:00
2016-01-22 17:37:41 -08:00
checkDiskSpace: checkDiskSpace,
readDkimPublicKeySync: readDkimPublicKeySync,
2017-01-02 13:00:30 +01:00
refreshDNS: refreshDNS,
2017-01-09 11:02:33 -08:00
configureAdmin: configureAdmin
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
2017-01-05 17:13:31 -08:00
certificates = require('./certificates.js'),
child_process = require('child_process'),
clients = require('./clients.js'),
config = require('./config.js'),
2016-05-11 09:54:51 -07:00
constants = require('./constants.js'),
2017-01-09 11:00:09 -08:00
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
2016-01-22 17:37:41 -08:00
df = require('node-df'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
2016-01-22 17:37:41 -08:00
mailer = require('./mailer.js'),
2017-01-05 17:13:31 -08:00
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
2016-07-04 22:30:25 -05:00
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
2017-01-09 10:49:34 -08:00
taskmanager = require('./taskmanager.js'),
tokendb = require('./tokendb.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
2016-06-07 09:59:29 -07:00
user = require('./user.js'),
util = require('util'),
_ = require('underscore');
2015-10-27 18:38:13 +01:00
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'),
2016-01-25 20:18:50 -08:00
RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
var IP_BASED_SETUP_NAME = 'ip_based_setup'; // This will be used for cert and nginx config file names
2015-10-29 12:28:50 -07:00
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// result to not depend on the appstore
const BOX_AND_USER_TEMPLATE = {
box: {
region: null,
size: null,
plan: 'Custom Plan'
},
user: {
billing: false,
currency: ''
}
};
var gUpdatingDns = false, // flag for dns update reentrancy
gBoxAndUserDetails = null, // cached cloudron details like region,size...
gConfigState = { dns: false, tls: false, configured: false };
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(CloudronError, Error);
CloudronError.BAD_FIELD = 'Field error';
CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
2017-01-12 11:00:46 -08:00
CloudronError.ALREADY_SETUP = 'Already Setup';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
installAppBundle,
checkConfigState
], callback);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
2017-01-09 10:49:34 -08:00
platform.events.removeListener(platform.EVENT_READY, onPlatformReady);
async.series([
2017-01-09 11:00:09 -08:00
cron.uninitialize,
2017-01-09 10:49:34 -08:00
taskmanager.pauseTasks,
mailer.stop,
platform.uninitialize
], callback);
}
function onConfigured(callback) {
callback = callback || NOOP_CALLBACK;
debug('onConfigured: current state: %j', gConfigState);
if (gConfigState.configured) return callback(); // re-entracy flag
gConfigState.configured = true;
2017-01-09 10:49:34 -08:00
platform.events.on(platform.EVENT_READY, onPlatformReady);
async.series([
2017-01-09 15:25:45 -08:00
clients.addDefaultClients,
2017-01-09 11:00:09 -08:00
cron.initialize,
certificates.ensureFallbackCertificate,
platform.initialize, // requires fallback certs in mail container
addDnsRecords,
configureAdmin,
2017-01-07 23:37:38 -08:00
mailer.start
], callback);
}
2017-01-09 10:49:34 -08:00
function onPlatformReady(callback) {
callback = callback || NOOP_CALLBACK;
debug('onPlatformReady');
async.series([
taskmanager.resumeTasks
], callback);
}
2017-01-05 19:01:44 -08:00
function getConfigStateSync() {
return gConfigState;
}
function checkConfigState(callback) {
callback = callback || NOOP_CALLBACK;
if (!config.fqdn()) {
settings.events.once(settings.DNS_CONFIG_KEY, function () { checkConfigState(); }); // check again later
return callback(null);
}
debug('checkConfigState: configured');
onConfigured(callback);
2015-10-27 16:11:33 -07:00
}
2017-01-12 11:00:46 -08:00
function dnsSetup(dnsConfig, domain, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
settings.setDnsConfig(dnsConfig, domain, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.set('fqdn', domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
onConfigured(); // do not block
2017-01-12 11:00:46 -08:00
callback();
});
}
2017-01-05 17:13:31 -08:00
function configureAdmin(callback) {
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
debug('configureAdmin');
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
var certFilePath = path.join(paths.NGINX_CERT_DIR, IP_BASED_SETUP_NAME + '-' + ip + '.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, IP_BASED_SETUP_NAME + '-' + ip + '.key');
2017-01-05 17:13:31 -08:00
// check if we already have a cert for this IP, otherwise create one, this is mostly useful for servers with changing IPs
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureAdmin: create new cert for %s', ip);
2017-01-05 17:13:31 -08:00
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, ip);
safe.child_process.execSync(certCommand);
}
// always create a configuration for the ip
nginx.configureAdmin(certFilePath, keyFilePath, IP_BASED_SETUP_NAME + '.conf', '', function (error) {
if (error) return callback(error);
// skip my.domain.com setup if we don't have a domain
if (!config.fqdn()) return callback(null);
2017-01-05 17:13:31 -08:00
subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return callback(error);
2017-01-05 19:01:44 -08:00
gConfigState.dns = true;
2017-01-05 17:13:31 -08:00
certificates.ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
if (error) { // currently, this can never happen
debug('Error obtaining certificate. Proceed anyway', error);
return callback();
}
2017-01-05 19:01:44 -08:00
gConfigState.tls = true;
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
2017-01-05 17:13:31 -08:00
});
});
});
2017-01-05 17:13:31 -08:00
});
}
function setTimeZone(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setTimeZone ip:%s', ip);
// https://github.com/bluesmoon/node-geoip
// https://github.com/runk/node-maxmind
// { url: 'http://freegeoip.net/json/%s', jpath: 'time_zone' },
// { url: 'http://ip-api.com/json/%s', jpath: 'timezone' },
// { url: 'http://geoip.nekudo.com/api/%s', jpath: 'time_zone }
superagent.get('http://ip-api.com/json/' + ip).timeout(10 * 1000).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
2015-12-17 19:35:52 -08:00
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
2016-09-07 20:00:44 -07:00
if (!result.body.timezone || typeof result.body.timezone !== 'string') {
2015-09-18 12:03:48 -07:00
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
2016-09-07 20:00:44 -07:00
debug('Setting timezone to ', result.body.timezone);
2016-09-07 20:00:44 -07:00
settings.setTimeZone(result.body.timezone, callback);
});
}
function activate(username, password, email, displayName, ip, auditSource, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
2016-05-01 20:01:34 -07:00
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
2016-06-08 14:09:06 +02:00
clients.get('cid-webadmin', function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// Also generate a token so the admin creation can also act as a login
var token = tokendb.generateToken();
2016-08-01 10:14:45 +02:00
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
callback(null, { token: token, expires: expires });
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
2016-06-07 09:59:29 -07:00
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
activated: count !== 0,
version: config.version(),
boxVersionsUrl: config.get('boxVersionsUrl'),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
2016-01-13 16:09:36 -08:00
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.fqdn() ? config.adminFqdn() : null,
configState: gConfigState
});
});
});
}
2016-07-02 10:30:12 -05:00
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
// only supported for caas
if (config.provider() !== 'caas') return callback(null, {});
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
.query({ token: config.token() })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
gBoxAndUserDetails = result.body;
return callback(null, gBoxAndUserDetails);
});
}
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
2016-07-02 10:30:12 -05:00
getBoxAndUserDetails(function (error, result) {
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getDeveloperMode(function (error, developerMode) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
isDev: config.isDev(),
fqdn: config.fqdn(),
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.get(),
isCustomDomain: config.isCustomDomain(),
isDemo: config.isDemo(),
developerMode: developerMode,
region: result.box.region,
size: result.box.size,
billing: !!result.user.billing,
plan: result.box.plan,
currency: result.user.currency,
memory: os.totalmem(),
provider: config.provider(),
cloudronName: cloudronName
});
});
});
});
}
function sendHeartbeat() {
if (!config.token() || !config.fqdn()) return;
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
}
2016-11-15 15:24:40 +01:00
function sendAliveStatus(callback) {
if (typeof callback !== 'function') {
callback = function (error) {
if (error && error.reason !== CloudronError.INTERNAL_ERROR) console.error(error);
2016-11-15 15:24:40 +01:00
else if (error) debug(error);
};
}
2017-01-10 10:22:47 +01:00
function sendAliveStatusWithAppstoreConfig(backendSettings, appstoreConfig) {
assert.strictEqual(typeof backendSettings, 'object');
2016-11-15 15:24:40 +01:00
assert.strictEqual(typeof appstoreConfig.userId, 'string');
assert.strictEqual(typeof appstoreConfig.cloudronId, 'string');
assert.strictEqual(typeof appstoreConfig.token, 'string');
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId;
var data = {
domain: config.fqdn(),
2016-11-16 14:45:27 +01:00
version: config.version(),
2017-01-10 10:22:47 +01:00
provider: config.provider(),
backendSettings: backendSettings
2016-11-15 15:24:40 +01:00
};
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
}
2017-01-10 10:22:47 +01:00
settings.getAll(function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
2016-11-15 15:24:40 +01:00
2017-01-10 10:22:47 +01:00
var backendSettings = {
dnsConfig: {
provider: result[settings.DNS_CONFIG_KEY].provider,
wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined
},
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider
}
};
2016-11-15 15:24:40 +01:00
2017-01-10 10:22:47 +01:00
// Caas Cloudrons do not store appstore credentials in their local database
if (config.provider() === 'caas') {
if (!config.token()) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, 'no token set'));
2016-11-15 15:24:40 +01:00
2017-01-10 10:22:47 +01:00
var url = config.apiServerOrigin() + '/api/v1/exchangeBoxTokenWithUserToken';
superagent.post(url).query({ token: config.token() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
sendAliveStatusWithAppstoreConfig(backendSettings, result.body);
});
} else {
settings.getAppstoreConfig(function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (!result.token) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, 'not registered yet'));
sendAliveStatusWithAppstoreConfig(backendSettings, result);
});
}
});
2016-11-15 15:24:40 +01:00
}
function readDkimPublicKeySync() {
if (!config.fqdn()) {
debug('Cannot read dkim public key without a domain.', safe.error);
return null;
}
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
var dkimPublicKeyFile = path.join(dkimPath, 'public');
if (!fs.existsSync(dkimPrivateKeyFile) || !fs.existsSync(dkimPublicKeyFile)) {
debug('Generating new DKIM keys');
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
return null;
}
child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
} else {
debug('DKIM keys already present');
}
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) {
debug('Error reading dkim public key.', safe.error);
return null;
}
// remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join('');
return publicKey;
}
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
2015-10-30 13:53:12 -07:00
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
2015-10-29 15:35:07 -07:00
subdomains.get('', 'TXT', function (error, txtRecords) {
if (error) return callback(error);
2015-10-30 18:12:24 -07:00
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
2015-10-30 13:53:12 -07:00
var i, validSpf;
2015-10-29 15:35:07 -07:00
for (i = 0; i < txtRecords.length; i++) {
2015-10-30 16:04:09 -07:00
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
validSpf = txtRecords[i].indexOf(' a:' + config.adminFqdn() + ' ') !== -1;
break;
}
if (validSpf) return callback(null, null);
2015-10-29 15:35:07 -07:00
if (i == txtRecords.length) {
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ~all"';
} else {
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
}
2015-10-30 13:45:07 -07:00
return callback(null, txtRecords);
});
}
2017-01-02 13:00:30 +01:00
function addDnsRecords(callback) {
2017-01-02 13:14:03 +01:00
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
if (gUpdatingDns) {
debug('addDnsRecords: dns update already in progress');
return callback();
}
gUpdatingDns = true;
var dkimKey = readDkimPublicKeySync();
2016-05-04 09:28:12 -07:00
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
2016-01-05 12:16:48 +01:00
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
2016-05-11 09:54:51 -07:00
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
2016-01-05 12:16:48 +01:00
// t=s limits the domainkey to this domain and not it's subdomains
2016-12-14 14:54:17 +01:00
var dkimRecord = { subdomain: constants.DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
2016-01-05 12:16:48 +01:00
var records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
2017-01-11 00:08:13 +01:00
// for non-custom domains, we show a noapp.html page
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
2016-01-05 12:16:48 +01:00
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
}
2016-01-05 12:16:48 +01:00
debug('addDnsRecords: %j', records);
2016-01-05 12:16:48 +01:00
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
2016-01-05 12:16:48 +01:00
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
2015-10-30 13:48:46 -07:00
2016-01-05 12:16:48 +01:00
debug('addDnsRecords: will update %j', records);
2015-11-04 14:28:22 -08:00
2016-01-05 12:16:48 +01:00
async.mapSeries(records, function (record, iteratorCallback) {
subdomains.upsert(record.subdomain, record.type, record.values, iteratorCallback);
2016-01-05 12:16:48 +01:00
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
retryCallback(error);
});
2015-11-04 14:28:22 -08:00
});
2016-01-05 12:16:48 +01:00
}, function (error) {
gUpdatingDns = false;
2015-10-30 18:12:24 -07:00
2016-01-05 12:16:48 +01:00
debug('addDnsRecords: done updating records with error:', error);
2015-10-30 18:12:24 -07:00
2016-01-05 12:16:48 +01:00
callback(error);
});
2015-08-30 21:29:02 -07:00
});
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
2016-06-02 19:26:42 -07:00
function update(boxUpdateInfo, auditSource, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
2016-06-02 19:26:42 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!boxUpdateInfo) return callback(null);
var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
2016-06-02 19:26:42 -07:00
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
// ensure tools can 'wait' on progress
progress.set(progress.UPDATE, 0, 'Starting');
// initiate the update/upgrade but do not wait for it
if (boxUpdateInfo.upgrade) {
debug('Starting upgrade');
doUpgrade(boxUpdateInfo, function (error) {
if (error) {
console.error('Upgrade failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
} else {
debug('Starting update');
doUpdate(boxUpdateInfo, function (error) {
if (error) {
console.error('Update failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
}
callback(null);
}
function updateToLatest(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
// check if this is just a version number change
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
doShortCircuitUpdate(boxUpdateInfo, function (error) {
if (error) debug('Short-circuit update failed', error);
});
return callback(null);
}
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
2016-06-02 19:26:42 -07:00
update(boxUpdateInfo, auditSource, callback);
}
2016-01-25 16:46:54 -08:00
function doShortCircuitUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
debug('Starting short-circuit from prerelease version %s to release version %s', config.version(), boxUpdateInfo.version);
config.setVersion(boxUpdateInfo.version);
progress.clear(progress.UPDATE);
2016-01-25 16:51:14 -08:00
updateChecker.resetUpdateInfo();
2016-01-25 16:46:54 -08:00
callback();
}
function doUpgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
2015-07-28 14:40:22 -07:00
function upgradeError(e) {
progress.set(progress.UPDATE, -1, e.message);
2015-07-28 14:40:22 -07:00
callback(e);
}
2015-09-22 13:01:56 -07:00
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback();
2016-06-29 23:24:00 -05:00
retire('upgrade');
});
});
}
function doUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
function updateError(e) {
progress.set(progress.UPDATE, -1, e.message);
2015-07-28 14:40:22 -07:00
callback(e);
}
2015-09-22 13:01:56 -07:00
progress.set(progress.UPDATE, 5, 'Backing up for update');
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
if (error) return updateError(error);
// NOTE: this data is opaque and will be passed through the installer.sh
var data= {
provider: config.provider(),
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
tlsCert: config.tlsCert(),
tlsKey: config.tlsKey(),
isCustomDomain: config.isCustomDomain(),
isDemo: config.isDemo(),
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
version: boxUpdateInfo.version,
boxVersionsUrl: config.get('boxVersionsUrl')
};
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, data);
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
if (error) return updateError(error);
// Do not add any code here. The installer script will stop the box code any instant
});
});
}
2016-04-04 22:19:15 -07:00
function installAppBundle(callback) {
assert.strictEqual(typeof callback, 'function');
if (fs.existsSync(paths.FIRST_RUN_FILE)) return callback();
2016-04-04 22:19:15 -07:00
var bundle = config.get('appBundle');
debug('initialize: installing app bundle on first run: %j', bundle);
2016-04-04 22:19:15 -07:00
if (!bundle || bundle.length === 0) return callback();
2016-04-04 22:19:15 -07:00
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location);
2016-04-04 22:19:15 -07:00
var data = {
appStoreId: appInfo.appstoreId,
location: appInfo.location,
portBindings: appInfo.portBindings || null,
accessRestriction: appInfo.accessRestriction || null,
};
2016-06-03 23:22:38 -07:00
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
2016-04-04 22:19:15 -07:00
}, function (error) {
if (error) debug('autoInstallApps: ', error);
fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
2016-04-04 22:19:15 -07:00
callback();
});
}
2016-01-22 17:37:41 -08:00
function checkDiskSpace(callback) {
callback = callback || NOOP_CALLBACK;
2016-01-22 17:37:41 -08:00
debug('Checking disk space');
df(function (error, entries) {
if (error) {
debug('df error %s', error.message);
mailer.outOfDiskSpace(error.message);
return callback();
}
var oos = entries.some(function (entry) {
return (entry.mount === paths.DATA_DIR && entry.capacity >= 0.90) ||
2016-07-26 17:10:22 -07:00
(entry.mount === '/' && entry.available <= (1.25 * 1024 * 1024)); // 1.5G
2016-01-22 17:37:41 -08:00
});
debug('Disk space checked. ok: %s', !oos);
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
callback();
});
}
2016-01-25 16:03:12 -08:00
function retire(reason, info, callback) {
2016-06-29 23:24:00 -05:00
assert(reason === 'migrate' || reason === 'upgrade');
info = info || { };
callback = callback || NOOP_CALLBACK;
var data = {
2016-07-06 16:25:39 -05:00
apiServerOrigin: config.apiServerOrigin(),
isCustomDomain: config.isCustomDomain(),
fqdn: config.fqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
2016-01-25 20:18:50 -08:00
}
2016-07-04 22:30:25 -05:00
function doMigrate(options, callback) {
2016-07-02 16:03:21 -05:00
assert.strictEqual(typeof options, 'object');
2016-06-27 22:24:30 -05:00
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
function unlock(error) {
2016-06-28 16:09:44 -05:00
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
2016-06-28 16:21:22 -05:00
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
2016-06-27 22:24:30 -05:00
}
2016-06-28 16:16:15 -05:00
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
2016-06-28 15:34:04 -05:00
2016-06-27 22:24:30 -05:00
// initiate the migration in the background
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error, backupId) {
if (error) return unlock(error);
2016-07-02 16:03:21 -05:00
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
options.restoreKey = backupId;
2016-06-27 22:24:30 -05:00
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
2016-07-02 16:03:21 -05:00
.send(options)
.timeout(30 * 1000)
2016-06-27 22:24:30 -05:00
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network error
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
2016-06-28 16:09:44 -05:00
progress.set(progress.MIGRATE, 10, 'Migrating');
2016-06-28 15:43:34 -05:00
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
2016-06-27 22:24:30 -05:00
});
});
callback(null);
}
2016-07-04 22:30:25 -05:00
function migrate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.isDemo()) return callback(new CloudronError(CloudronError.BAD_FIELD, 'Not allowed in demo mode'));
2016-07-04 22:30:25 -05:00
if (!options.domain) return doMigrate(options, callback);
2016-07-04 23:21:14 -05:00
var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint');
2016-07-04 22:30:25 -05:00
2017-01-10 16:23:01 -08:00
settings.setDnsConfig(dnsConfig, options.domain, function (error) {
2016-07-04 22:30:25 -05:00
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
2017-01-10 16:02:11 -08:00
// TODO: should probably rollback dns config if migrate fails
2016-07-04 22:30:25 -05:00
doMigrate(options, callback);
});
}
2017-01-02 13:00:30 +01:00
function refreshDNS(callback) {
2017-01-02 13:14:03 +01:00
callback = callback || NOOP_CALLBACK;
2017-01-02 13:00:30 +01:00
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('refreshDNS: current ip %s', ip);
addDnsRecords(function (error) {
if (error) return callback(error);
debug('refreshDNS: done for system records');
2017-01-02 13:00:30 +01:00
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, function (app, callback) {
// get the current record before updating it
subdomains.get(app.location, 'A', function (error, values) {
if (error) return callback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !app.dnsRecordId) return callback(null, new Error('DNS Record already exists'));
subdomains.upsert(app.location, 'A', [ ip ], callback);
});
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
2017-01-02 13:00:30 +01:00
});
});
}