diff --git a/setup/start/sudoers b/setup/start/sudoers index 2d638745c..5506ac169 100644 --- a/setup/start/sudoers +++ b/setup/start/sudoers @@ -34,3 +34,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrot Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js +Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV" +yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh + diff --git a/src/cloudron.js b/src/cloudron.js index d0ef1bf3b..f0cb7aef6 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -15,6 +15,7 @@ exports = module.exports = { sendCaasHeartbeat: sendCaasHeartbeat, updateToLatest: updateToLatest, + restore: restore, reboot: reboot, retire: retire, migrate: migrate, @@ -31,6 +32,7 @@ var appdb = require('./appdb.js'), assert = require('assert'), async = require('async'), backups = require('./backups.js'), + BackupsError = require('./backups.js').BackupsError, certificates = require('./certificates.js'), child_process = require('child_process'), clients = require('./clients.js'), @@ -69,7 +71,8 @@ var appdb = require('./appdb.js'), var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'), UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'), - RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh'); + RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh'), + RESTART_CMD = path.join(__dirname, 'scripts/restart.sh'); var NOOP_CALLBACK = function (error) { if (error) debug(error); }; @@ -87,7 +90,7 @@ const BOX_AND_USER_TEMPLATE = { }; var gBoxAndUserDetails = null, // cached cloudron details like region,size... - gWebadminStatus = { dns: false, tls: false, configuring: false }; + gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false }; function CloudronError(reason, errorOrMessage) { assert.strictEqual(typeof reason, 'string'); @@ -121,7 +124,7 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported'; function initialize(callback) { assert.strictEqual(typeof callback, 'function'); - gWebadminStatus = { dns: false, tls: false, configuring: false }; + gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false }; gBoxAndUserDetails = null; async.series([ @@ -609,6 +612,38 @@ function addDnsRecords(ip, callback) { }); } +function restore(backupConfig, backupId, version, callback) { + assert.strictEqual(typeof backupConfig, 'object'); + assert.strictEqual(typeof backupId, 'string'); + assert.strictEqual(typeof version, 'string'); + assert.strictEqual(typeof callback, 'function'); + + if (config.version() !== version) return callback(new CloudronError(CloudronError.BAD_STATE, 'Bad version')); + + user.count(function (error, count) { + if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); + if (count) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED, 'Already activated')); + + backups.testConfig(backupConfig, function (error) { + if (error && error.reason === BackupsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message)); + if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message)); + if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); + + gWebadminStatus.restoring = true; + + backups.restore(backupConfig, backupId, function (error) { + gWebadminStatus.restoring = false; + + if (error) return debug('Error restoring:', error); + + shell.sudo('restart', [ RESTART_CMD ], NOOP_CALLBACK); + }); + + callback(null); // do no block + }); + }); +} + function reboot(callback) { shell.sudo('reboot', [ REBOOT_CMD ], callback); } diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index 11b5c26a2..dc6b5a6ed 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -6,6 +6,7 @@ exports = module.exports = { setupTokenAuth: setupTokenAuth, providerTokenAuth: providerTokenAuth, getStatus: getStatus, + restore: restore, reboot: reboot, migrate: migrate, getProgress: getProgress, @@ -78,6 +79,31 @@ function activate(req, res, next) { }); } +function restore(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + if (!req.body.backupConfig || typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig is required')); + + var backupConfig = req.body.backupConfig; + if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required')); + if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string')); + if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string')); + if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean')); + + if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null')); + if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string')); + + cloudron.restore(backupConfig, req.body.backupId, req.body.version, function (error) { + if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message)); + if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message)); + if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message)); + if (error && error.reason === CloudronError.EXTERNAL_ERROR) return next(new HttpError(402, error.message)); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200)); + }); +} + function dnsSetup(req, res, next) { assert.strictEqual(typeof req.body, 'object'); diff --git a/src/scripts/restart.sh b/src/scripts/restart.sh new file mode 100755 index 000000000..3a5caad06 --- /dev/null +++ b/src/scripts/restart.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -eu -o pipefail + +readonly INFRA_VERSION_FILE=/home/yellowtent/platformdata/INFRA_VERSION + +if [[ ${EUID} -ne 0 ]]; then + echo "This script should be run as root." > /dev/stderr + exit 1 +fi + +if [[ $# == 1 && "$1" == "--check" ]]; then + echo "OK" + exit 0 +fi + +if [[ "${BOX_ENV}" == "cloudron" ]]; then + systemctl restart box +fi + diff --git a/src/server.js b/src/server.js index 8350a4908..6e355eccf 100644 --- a/src/server.js +++ b/src/server.js @@ -98,8 +98,10 @@ function initializeExpressSync() { var csrf = routes.oauth2.csrf; // public routes - router.post('/api/v1/cloudron/activate', routes.cloudron.setupTokenAuth, routes.cloudron.activate); router.post('/api/v1/cloudron/dns_setup', routes.cloudron.providerTokenAuth, routes.cloudron.dnsSetup); // only available until no-domain + router.post('/api/v1/cloudron/activate', routes.cloudron.setupTokenAuth, routes.cloudron.activate); + router.post('/api/v1/cloudron/restore', routes.cloudron.restore); // only available until activated + router.get ('/api/v1/cloudron/progress', routes.cloudron.getProgress); router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus); router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar