Files
cloudron-box/src/provision.js
T
Girish Ramakrishnan d732adf34b restore: complete the latest full backup task of site id
when creating a full backup, the task is still running and the database
dump which is part of the backup, still says it is still running.

during restore, we mark the taskId as completed. detection of the task
is using the siteId in config (when restoring via backup config) or
just blindly setting the last backup task as completed (in manual form fill).
2025-10-06 19:50:58 +02:00

279 lines
12 KiB
JavaScript

'use strict';
exports = module.exports = {
setup,
restore,
activate,
getStatus,
};
const appstore = require('./appstore.js'),
assert = require('node:assert'),
backupSites = require('./backupsites.js'),
backups = require('./backups.js'),
backuptask = require('./backuptask.js'),
BoxError = require('./boxerror.js'),
dashboard = require('./dashboard.js'),
constants = require('./constants.js'),
debug = require('debug')('box:provision'),
dns = require('./dns.js'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
fs = require('node:fs'),
mail = require('./mail.js'),
mailServer = require('./mailserver.js'),
network = require('./network.js'),
oidcClients = require('./oidcclients.js'),
platform = require('./platform.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
shell = require('./shell.js')('provision'),
semver = require('semver'),
paths = require('./paths.js'),
system = require('./system.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
tld = require('tldjs'),
tokens = require('./tokens.js');
// we cannot use tasks since the tasks table gets overwritten when db is imported
const gStatus = {
setup: {
active: false,
message: '',
errorMessage: null,
startTime: null
},
restore: {
active: false,
message: '',
errorMessage: null,
startTime: null
},
activated: false,
adminFqdn: null,
provider: null,
version: constants.VERSION // for reloading dashboard
};
function setProgress(task, message) {
debug(`setProgress: ${task} - ${message}`);
gStatus[task].message = message;
}
async function ensureDhparams() {
if (fs.existsSync(paths.DHPARAMS_FILE)) return;
debug('ensureDhparams: generating dhparams');
const dhparams = await shell.spawn('openssl', ['dhparam', '-dsaparam', '2048'], { encoding: 'utf8' });
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
}
async function unprovision() {
// TODO: also cancel any existing configureWebadmin task
await dashboard.clearLocation();
await mail.clearDomains();
await domains.clear();
}
async function setupTask(domain, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
const location = { subdomain: constants.DASHBOARD_SUBDOMAIN, domain };
try {
debug(`setupTask: subdomain ${location.subdomain} and domain ${location.domain}`);
await dns.registerLocations([location], { overwriteDns: true }, (progress) => setProgress('setup', progress.message));
await dns.waitForLocations([location], (progress) => setProgress('setup', progress.message));
await reverseProxy.ensureCertificate(location, {}, auditSource);
await ensureDhparams();
await dashboard.setupLocation(constants.DASHBOARD_SUBDOMAIN, domain, auditSource);
await backupSites.addDefault(auditSource);
setProgress('setup', 'Done'),
await eventlog.add(eventlog.ACTION_PROVISION, auditSource, {});
} catch (error) {
debug('setupTask: error. %o', error);
gStatus.setup.errorMessage = error.message;
}
gStatus.setup.active = false;
}
async function setup(domainConfig, ipv4Config, ipv6Config, auditSource) {
assert.strictEqual(typeof domainConfig, 'object');
assert.strictEqual(typeof ipv4Config, 'object');
assert.strictEqual(typeof ipv6Config, 'object');
assert.strictEqual(typeof auditSource, 'object');
if (gStatus.setup.active || gStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring');
gStatus.setup = { active: true, errorMessage: '', message: 'Adding domain', startTime: (new Date()).toISOString() };
try {
const activated = await users.isActivated();
if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated');
await unprovision();
const domain = domainConfig.domain.toLowerCase();
const zoneName = domainConfig.zoneName ? domainConfig.zoneName : (tld.getDomain(domain) || domain);
debug(`setup: domain ${domain} and zone ${zoneName}`);
const data = {
zoneName: zoneName,
provider: domainConfig.provider,
config: domainConfig.config,
fallbackCertificate: domainConfig.fallbackCertificate || null,
tlsConfig: domainConfig.tlsConfig || { provider: 'letsencrypt-prod' },
dkimSelector: 'cloudron'
};
await mailServer.setLocation(constants.DASHBOARD_SUBDOMAIN, domain); // default mail location. do this before we add the domain for upserting mail DNS
await domains.add(domain, data, auditSource);
await network.setIPv4Config(ipv4Config);
await network.setIPv6Config(ipv6Config);
safe(setupTask(domain, auditSource), { debug }); // now that args are validated run the task in the background
} catch (error) {
debug('setup: error. %o', error);
gStatus.setup.active = false;
gStatus.setup.errorMessage = error.message;
throw error;
}
}
async function activate(username, password, email, displayName, ip, auditSource) {
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');
debug(`activate: user: ${username} email:${email}`);
await appstore.registerCloudron3();
const [error, ownerId] = await safe(users.createOwner(email, username, password, displayName, auditSource));
if (error && error.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Already activated');
if (error) throw error;
const token = { clientId: oidcClients.ID_WEBADMIN, identifier: ownerId, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS };
const result = await tokens.add(token);
await eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, {});
setImmediate(() => safe(platform.onActivated({ skipDnsSetup: false }), { debug }));
return {
userId: ownerId,
token: result.accessToken,
expires: result.expires
};
}
async function restoreTask(backupSite, remotePath, ipv4Config, ipv6Config, options, auditSource) {
assert.strictEqual(typeof backupSite, 'object');
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof ipv4Config, 'object');
assert.strictEqual(typeof ipv6Config, 'object');
assert.strictEqual(typeof options, 'object'); // { skipDnsSetup }
assert.strictEqual(typeof auditSource, 'object');
try {
setProgress('restore', 'Preparing backup site');
await backupSites.storageApi(backupSite).setup(backupSite.config);
setProgress('restore', 'Downloading box backup');
await backuptask.restore(backupSite, remotePath, (progress) => setProgress('restore', progress.message));
setProgress('restore', 'Downloading mail backup');
const mailBackups = await backups.getByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_MAIL, backups.BACKUP_STATE_NORMAL, 1, 1);
if (mailBackups.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'mail backup not found');
const mailRemotePath = mailBackups[0].remotePath;
await backuptask.downloadMail(backupSite, mailRemotePath, (progress) => setProgress('restore', progress.message));
await ensureDhparams();
await network.setIPv4Config(ipv4Config);
await network.setIPv6Config(ipv6Config);
await reverseProxy.restoreFallbackCertificates();
await backupSites.reinitAll();
// when creating a sql dump during a full backup, the task has not completed yet. we mark it as completed here for the Sites UI to not indicate a crash
// siteId can be missing when restoring manually instead of via the backup config
const backupTasks = await tasks.list(1, 1, {
type: options.siteId ? tasks.TASK_FULL_BACKUP_PREFIX + options.siteId : null,
prefix: !options.siteId ? tasks.TASK_FULL_BACKUP_PREFIX : null // guess that the latest full backup was the site that was used
});
await tasks.setCompleted(backupTasks[0].id, { error: null });
const location = await dashboard.getLocation(); // load this fresh from after the backup.restore
if (!options.skipDnsSetup) {
await dns.registerLocations([location], { overwriteDns: true }, (progress) => setProgress('restore', progress.message));
await dns.waitForLocations([location], (progress) => setProgress('setup', progress.message));
await reverseProxy.ensureCertificate(location, {}, auditSource);
}
await dashboard.setupLocation(location.subdomain, location.domain, auditSource);
await eventlog.add(eventlog.ACTION_RESTORE, auditSource, { remotePath });
await appstore.checkSubscription(); // never throws. worst case, user has to visit the Account view to refresh subscription info
setImmediate(() => safe(platform.onActivated({ backupSite, skipDnsSetup: options.skipDnsSetup }), { debug }));
} catch (error) {
debug('restoreTask: error. %o', error);
gStatus.restore.errorMessage = error ? error.message : '';
}
gStatus.restore.active = false;
}
async function restore(backupConfig, remotePath, version, ipv4Config, ipv6Config, options, auditSource) {
assert.strictEqual(typeof backupConfig, 'object'); // { format, config, provider, encryptionPassword }
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof ipv4Config, 'object');
assert.strictEqual(typeof ipv6Config, 'object');
assert.strictEqual(typeof options, 'object'); // { skipDnsSetup, siteId }
assert.strictEqual(typeof auditSource, 'object');
if (!semver.valid(version)) throw new BoxError(BoxError.BAD_FIELD, 'version is not a valid semver');
if (constants.VERSION !== version) throw new BoxError(BoxError.BAD_STATE, `Run "cloudron-setup --version ${version}" on a fresh Ubuntu installation to restore from this backup`);
if (gStatus.setup.active || gStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring');
gStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config', startTime: (new Date()).toISOString() };
try {
const activated = await users.isActivated();
if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.');
const backupSite = await backupSites.createPseudo({
id: `cloudron-restore`,
provider: backupConfig.provider,
config: backupConfig.config,
format: backupConfig.format,
encryptionPassword: backupConfig.encryptionPassword ?? null,
encryptedFilenames: !!backupConfig.encryptedFilenames
});
const error = await network.testIPv4Config(ipv4Config);
if (error) throw error;
safe(restoreTask(backupSite, remotePath, ipv4Config, ipv6Config, options, auditSource), { debug }); // now that args are validated run the task in the background
} catch (error) {
debug('restore: error. %o', error);
gStatus.restore.active = false;
gStatus.restore.errorMessage = error.message;
throw error;
}
}
async function getStatus() {
const { fqdn:dashboardFqdn } = await dashboard.getLocation();
gStatus.adminFqdn = dashboardFqdn; // indicator for main.js if dnssetup was already done but not activated
gStatus.activated = await users.isActivated(); // indicator for admin setup
gStatus.provider = await system.getProvider(); // preselect dns provider and ami token
return gStatus;
}