Files
cloudron-box/src/provision.js

270 lines
11 KiB
JavaScript
Raw Normal View History

2018-01-29 15:47:26 -08:00
'use strict';
exports = module.exports = {
2021-05-01 11:21:09 -07:00
setup,
restore,
activate,
getStatus
2018-01-29 15:47:26 -08:00
};
2021-05-04 21:40:11 -07:00
const assert = require('assert'),
2018-01-29 15:47:26 -08:00
async = require('async'),
backups = require('./backups.js'),
2019-10-22 20:36:20 -07:00
BoxError = require('./boxerror.js'),
branding = require('./branding.js'),
2018-01-29 15:47:26 -08:00
constants = require('./constants.js'),
cloudron = require('./cloudron.js'),
2018-12-11 15:29:47 -08:00
debug = require('debug')('box:provision'),
2018-01-29 15:47:26 -08:00
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
mounts = require('./mounts.js'),
2021-05-04 21:40:11 -07:00
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
2018-01-29 15:47:26 -08:00
semver = require('semver'),
settings = require('./settings.js'),
2019-11-11 16:05:53 -08:00
sysinfo = require('./sysinfo.js'),
2018-04-29 10:58:45 -07:00
users = require('./users.js'),
2018-01-29 15:47:26 -08:00
tld = require('tldjs'),
2020-02-06 16:57:33 +01:00
tokens = require('./tokens.js'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// we cannot use tasks since the tasks table gets overwritten when db is imported
let gProvisionStatus = {
setup: {
active: false,
message: '',
errorMessage: null
},
restore: {
active: false,
message: '',
errorMessage: null
}
};
2018-01-29 15:47:26 -08:00
function setProgress(task, message, callback) {
2018-12-22 20:48:03 -08:00
debug(`setProgress: ${task} - ${message}`);
gProvisionStatus[task].message = message;
callback();
}
function unprovision(callback) {
2018-01-29 15:47:26 -08:00
assert.strictEqual(typeof callback, 'function');
debug('unprovision');
2018-01-29 15:47:26 -08:00
// TODO: also cancel any existing configureWebadmin task
async.series([
2021-05-05 12:29:04 -07:00
settings.setDashboardLocation.bind(null, '', ''),
mail.clearDomains,
domains.clear
], callback);
}
function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof sysinfoConfig, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
2018-01-29 15:47:26 -08:00
2019-10-23 15:45:09 -07:00
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'));
2018-01-29 15:47:26 -08:00
gProvisionStatus.setup = { active: true, errorMessage: '', message: 'Adding domain' };
2018-01-29 15:47:26 -08:00
function done(error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
callback(error);
}
users.isActivated(function (error, activated) {
2019-10-24 20:31:45 -07:00
if (error) return done(error);
2019-10-23 15:45:09 -07:00
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated', { activate: true }));
unprovision(function (error) {
2019-10-23 15:45:09 -07:00
if (error) return done(error);
2018-11-10 00:43:46 -08:00
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName}`);
2018-11-10 00:43:46 -08:00
let data = {
zoneName: zoneName,
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' },
dkimSelector: 'cloudron'
};
async.series([
2021-05-05 12:29:04 -07:00
settings.setMailLocation.bind(null, domain, `${constants.DASHBOARD_LOCATION}.${domain}`), // default mail location. do this before we add the domain for upserting mail DNS
domains.add.bind(null, domain, data, auditSource),
sysinfo.testConfig.bind(null, sysinfoConfig)
], function (error) {
2019-10-23 15:45:09 -07:00
if (error) return done(error);
callback(); // now that args are validated run the task in the background
async.series([
settings.setSysinfoConfig.bind(null, sysinfoConfig),
2021-05-05 12:29:04 -07:00
cloudron.setupDnsAndCert.bind(null, constants.DASHBOARD_LOCATION, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
setProgress.bind(null, 'setup', 'Done'),
2021-06-03 11:42:32 -07:00
async () => eventlog.add(eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
});
});
});
2018-01-29 15:47:26 -08:00
});
}
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);
2021-06-04 09:28:40 -07:00
users.createOwner(username, password, email, displayName, auditSource, async function (error, userObject) {
2019-10-24 20:31:45 -07:00
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Already activated'));
if (error) return callback(error);
2018-01-29 15:47:26 -08:00
2021-06-04 09:28:40 -07:00
const token = { clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS };
let result;
[error, result] = await safe(tokens.add(token));
if (error) return callback(error);
2018-01-29 15:47:26 -08:00
2021-06-04 09:28:40 -07:00
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
2018-01-29 15:47:26 -08:00
2021-06-04 09:28:40 -07:00
callback(null, {
userId: userObject.id,
token: result.accessToken,
expires: result.expires
2018-01-29 15:47:26 -08:00
});
2021-06-04 09:28:40 -07:00
setImmediate(cloudron.onActivated.bind(null, {}, NOOP_CALLBACK)); // hack for now to not block the above http response
2018-01-29 15:47:26 -08:00
});
}
function restore(backupConfig, backupId, version, sysinfoConfig, options, auditSource, callback) {
2018-01-29 15:47:26 -08:00
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
2019-11-11 09:49:18 -08:00
assert.strictEqual(typeof sysinfoConfig, 'object');
assert.strictEqual(typeof options, 'object');
2018-11-10 00:43:46 -08:00
assert.strictEqual(typeof auditSource, 'object');
2018-01-29 15:47:26 -08:00
assert.strictEqual(typeof callback, 'function');
2019-10-23 15:45:09 -07:00
if (!semver.valid(version)) return callback(new BoxError(BoxError.BAD_FIELD, 'version is not a valid semver', { field: 'version' }));
2020-08-14 10:27:23 -07:00
if (constants.VERSION !== version) return callback(new BoxError(BoxError.BAD_STATE, `Run "cloudron-setup --version ${version}" on a fresh Ubuntu installation to restore from this backup`));
2018-01-29 15:47:26 -08:00
2019-10-23 15:45:09 -07:00
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'));
gProvisionStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config' };
function done(error) {
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
callback(error);
}
users.isActivated(async function (error, activated) {
2019-10-23 15:45:09 -07:00
if (error) return done(error);
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'));
2018-01-29 15:47:26 -08:00
if (backups.isMountProvider(backupConfig.provider)) {
error = mounts.validateMountOptions(backupConfig.provider, backupConfig.mountOptions);
if (error) return done(error);
const newMount = {
name: 'backup',
hostPath: backupConfig.mountPoint,
mountType: backupConfig.provider,
mountOptions: backupConfig.mountOptions
};
2021-06-21 22:37:32 -07:00
[error] = await safe(mounts.tryAddMount(newMount, { timeout: 10 })); // 10 seconds
if (error) return done(error);
}
backups.testProviderConfig(backupConfig, function (error) {
2019-10-23 15:45:09 -07:00
if (error) return done(error);
2018-01-29 15:47:26 -08:00
if ('password' in backupConfig) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
delete backupConfig.password;
2020-05-18 09:07:18 -07:00
} else {
backupConfig.encryption = null;
}
2019-11-11 16:05:53 -08:00
sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return done(error);
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
2018-01-29 15:47:26 -08:00
2019-11-11 16:05:53 -08:00
callback(); // now that the fields are validated, continue task in the background
2018-01-29 15:47:26 -08:00
2019-11-11 16:05:53 -08:00
async.series([
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setSysinfoConfig.bind(null, sysinfoConfig),
2021-05-04 21:40:11 -07:00
reverseProxy.restoreFallbackCertificates,
2020-08-15 23:00:39 -07:00
(done) => {
2021-05-05 13:14:48 -07:00
const dashboardDomain = settings.dashboardDomain(); // load this fresh from after the backup.restore
2020-08-15 23:00:39 -07:00
async.series([
(next) => {
if (options.skipDnsSetup) return next();
2021-05-05 13:14:48 -07:00
cloudron.setupDnsAndCert(constants.DASHBOARD_LOCATION, dashboardDomain, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK), next);
},
2021-05-05 13:14:48 -07:00
cloudron.setDashboardDomain.bind(null, dashboardDomain, auditSource)
2020-08-15 23:00:39 -07:00
], done);
},
settings.setBackupCredentials.bind(null, backupConfig), // update just the credentials and not the policy and flags
2021-06-03 11:42:32 -07:00
async () => eventlog.add(eventlog.ACTION_RESTORE, auditSource, { backupId }),
2019-11-11 16:05:53 -08:00
], function (error) {
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
if (!error) cloudron.onActivated(options, NOOP_CALLBACK);
2019-11-11 16:05:53 -08:00
});
2018-01-29 15:47:26 -08:00
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
2019-10-23 15:45:09 -07:00
if (error) return callback(error);
settings.getAll(function (error, allSettings) {
2019-10-23 15:45:09 -07:00
if (error) return callback(error);
callback(null, _.extend({
version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
2021-05-05 12:29:04 -07:00
adminFqdn: settings.dashboardDomain() ? settings.dashboardFqdn() : null,
language: allSettings[settings.LANGUAGE_KEY],
activated: activated,
2020-07-13 08:45:24 -07:00
provider: settings.provider() // used by setup wizard of marketplace images
}, gProvisionStatus));
});
});
}