backups: fix mounting logic of backup settings and cloudron restore
This commit is contained in:
@@ -31,6 +31,7 @@ exports = module.exports = {
|
||||
configureCollectd,
|
||||
|
||||
generateEncryptionKeysSync,
|
||||
isMountProvider,
|
||||
|
||||
BACKUP_IDENTIFIER_BOX: 'box',
|
||||
|
||||
@@ -116,6 +117,10 @@ function api(provider) {
|
||||
}
|
||||
}
|
||||
|
||||
function isMountProvider(provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'ext4';
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if ('password' in newConfig) {
|
||||
if (newConfig.password === constants.SECRET_PLACEHOLDER) {
|
||||
|
||||
+18
-1
@@ -18,7 +18,9 @@ const assert = require('assert'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
mail = require('./mail.js'),
|
||||
mounts = require('./mounts.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
@@ -177,10 +179,25 @@ function restore(backupConfig, backupId, version, sysinfoConfig, options, auditS
|
||||
callback(error);
|
||||
}
|
||||
|
||||
users.isActivated(function (error, activated) {
|
||||
users.isActivated(async function (error, activated) {
|
||||
if (error) return done(error);
|
||||
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'));
|
||||
|
||||
if (backups.isMountProvider(backupConfig.provider)) {
|
||||
error = mounts.validateMountOptions(backupConfig.provider, backupConfig.mountOptions);
|
||||
if (error) return callback(error);
|
||||
|
||||
const newMount = {
|
||||
name: 'backup',
|
||||
hostPath: backupConfig.mountPoint,
|
||||
mountType: backupConfig.provider,
|
||||
mountOptions: backupConfig.mountOptions
|
||||
};
|
||||
|
||||
[error] = await safe(mounts.tryAddMount(newMount, null, { times: 20, interval: 500 })); // 10 seconds
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
backups.testProviderConfig(backupConfig, function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
|
||||
@@ -30,4 +30,9 @@ systemctl stop "${mount_filename}" || true
|
||||
echo "$mount_file_contents" > "${mount_file}"
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# systemd can automatically create the "where" dir but the backup logic relies on permissions
|
||||
mkdir -p "${where}"
|
||||
chown yellowtent:yellowtent "${where}"
|
||||
|
||||
systemctl enable --no-block --now "${mount_filename}" || true
|
||||
|
||||
+39
-21
@@ -393,14 +393,45 @@ function getBackupConfig(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function mountOptionsChanged(currentConfig, backupConfig) {
|
||||
return currentConfig.provider !== backupConfig.provider
|
||||
|| currentConfig.mountPoint !== backupConfig.mountPoint
|
||||
|| !_.isEqual(currentConfig.mountOptions, backupConfig.mountOptions);
|
||||
}
|
||||
|
||||
function setBackupConfig(backupConfig, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getBackupConfig(function (error, currentConfig) {
|
||||
getBackupConfig(async function (error, oldConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
backups.injectPrivateFields(backupConfig, currentConfig);
|
||||
backups.injectPrivateFields(backupConfig, oldConfig);
|
||||
|
||||
let oldMount = null, newMount = null;
|
||||
if (backups.isMountProvider(oldConfig.provider)) {
|
||||
oldMount = {
|
||||
name: 'backup',
|
||||
hostPath: oldConfig.mountPoint,
|
||||
mountType: oldConfig.provider,
|
||||
mountOptions: oldConfig.mountOptions
|
||||
};
|
||||
}
|
||||
|
||||
if (backups.isMountProvider(backupConfig.provider) && (!backups.isMountProvider(oldConfig.provider) || mountOptionsChanged(oldConfig, backupConfig))) {
|
||||
error = mounts.validateMountOptions(backupConfig.provider, backupConfig.mountOptions);
|
||||
if (error) return callback(error);
|
||||
|
||||
newMount = {
|
||||
name: 'backup',
|
||||
hostPath: backupConfig.mountPoint,
|
||||
mountType: backupConfig.provider,
|
||||
mountOptions: backupConfig.mountOptions
|
||||
};
|
||||
|
||||
[error] = await safe(mounts.tryAddMount(newMount, oldMount, { times: 20, interval: 500 })); // 10 seconds
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
backups.testConfig(backupConfig, async function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -411,31 +442,18 @@ function setBackupConfig(backupConfig, callback) {
|
||||
}
|
||||
|
||||
// if any of these changes, we have to clear the cache
|
||||
if ([ 'format', 'provider', 'prefix', 'bucket', 'region', 'endpoint', 'backupFolder', 'mountPoint', 'encryption' ].some(p => backupConfig[p] !== currentConfig[p])) {
|
||||
if ([ 'format', 'provider', 'prefix', 'bucket', 'region', 'endpoint', 'backupFolder', 'mountPoint', 'encryption' ].some(p => backupConfig[p] !== oldConfig[p])) {
|
||||
debug('setBackupConfig: clearing backup cache');
|
||||
backups.cleanupCacheFilesSync();
|
||||
}
|
||||
|
||||
if (backupConfig.provider === 'sshfs' || backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'ext4') {
|
||||
error = mounts.validateMountOptions(backupConfig.provider, backupConfig.mountOptions);
|
||||
settingsdb.set(exports.BACKUP_CONFIG_KEY, JSON.stringify(backupConfig), async function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const backupVolume = {
|
||||
name: 'backup',
|
||||
hostPath: backupConfig.mountPoint,
|
||||
mountType: backupConfig.provider,
|
||||
mountOptions: backupConfig.mountOptions
|
||||
};
|
||||
|
||||
[error] = await safe(mounts.writeMountFile(backupVolume));
|
||||
if (error) return callback(error);
|
||||
} else if (currentConfig.provider === 'sshfs' || currentConfig.provider === 'cifs' || currentConfig.provider === 'nfs' || currentConfig.provider === 'ext4') {
|
||||
debug('setBackupConfig: removing old mount configuration');
|
||||
await safe(mounts.removeMountFile(currentConfig.hostPath));
|
||||
}
|
||||
|
||||
settingsdb.set(exports.BACKUP_CONFIG_KEY, JSON.stringify(backupConfig), function (error) {
|
||||
if (error) return callback(error);
|
||||
if (oldMount && (!backups.isMountProvider(backupConfig.provider) || (newMount && newMount.hostPath !== oldMount.hostPath))) {
|
||||
debug('setBackupConfig: removing old mount configuration');
|
||||
await safe(mounts.removeMountFile(oldMount.hostPath));
|
||||
}
|
||||
|
||||
notifyChange(exports.BACKUP_CONFIG_KEY, backupConfig);
|
||||
|
||||
|
||||
+12
-22
@@ -77,7 +77,7 @@ function checkPreconditions(apiConfig, dataLayout, callback) {
|
||||
df.file(getBackupPath(apiConfig)).then(function (result) {
|
||||
|
||||
// Check filesystem is mounted so we don't write into the actual folder on disk
|
||||
if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS) {
|
||||
if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS || apiConfig.provider === PROVIDER_EXT4) {
|
||||
if (result.mountpoint !== apiConfig.mountPoint) return callback(new BoxError(BoxError.FS_ERROR, `${apiConfig.mountPoint} is not mounted`));
|
||||
} else if (apiConfig.provider === PROVIDER_MOUNTPOINT) {
|
||||
if (result.mountpoint === '/') return callback(new BoxError(BoxError.FS_ERROR, `${apiConfig.backupFolder} is not mounted`));
|
||||
@@ -287,32 +287,22 @@ function testConfig(apiConfig, callback) {
|
||||
if (path.isAbsolute(apiConfig.prefix)) return new BoxError(BoxError.BAD_FIELD, 'prefix must be a relative path', { field: 'backupFolder' });
|
||||
if (path.normalize(apiConfig.prefix) !== apiConfig.prefix) return callback(new BoxError(BoxError.BAD_FIELD, 'prefix must contain a normalized relative path', { field: 'prefix' }));
|
||||
}
|
||||
|
||||
const mounts = safe.fs.readFileSync('/proc/mounts', 'utf8');
|
||||
const mountInfo = mounts.split('\n').filter(function (l) { return l.indexOf(apiConfig.mountPoint) !== -1; })[0];
|
||||
if (!mountInfo) return callback(new BoxError(BoxError.BAD_FIELD, `${apiConfig.mountPoint} is not mounted`, { field: 'mountPoint' }));
|
||||
}
|
||||
|
||||
if (apiConfig.provider === PROVIDER_FILESYSTEM || apiConfig.provider === PROVIDER_MOUNTPOINT) {
|
||||
const backupPath = getBackupPath(apiConfig);
|
||||
const field = apiConfig.provider === PROVIDER_FILESYSTEM ? 'backupFolder' : 'mountPoint';
|
||||
const backupPath = getBackupPath(apiConfig);
|
||||
const field = apiConfig.provider === PROVIDER_FILESYSTEM ? 'backupFolder' : 'mountPoint';
|
||||
|
||||
const stat = safe.fs.statSync(backupPath);
|
||||
if (!stat) return callback(new BoxError(BoxError.BAD_FIELD, 'Directory does not exist or cannot be accessed: ' + safe.error.message), { field });
|
||||
if (!stat.isDirectory()) return callback(new BoxError(BoxError.BAD_FIELD, 'Backup location is not a directory', { field }));
|
||||
if (!safe.fs.mkdirSync(path.join(backupPath, 'snapshot'), { recursive: true }) && safe.error.code !== 'EEXIST') {
|
||||
if (safe.error && safe.error.code === 'EACCES') return callback(new BoxError(BoxError.BAD_FIELD, `Access denied. Run "chown yellowtent:yellowtent ${backupPath}" on the server`, { field }));
|
||||
return callback(new BoxError(BoxError.BAD_FIELD, safe.error.message, { field }));
|
||||
}
|
||||
|
||||
if (!safe.fs.mkdirSync(path.join(backupPath, 'snapshot')) && safe.error.code !== 'EEXIST') {
|
||||
if (safe.error && safe.error.code === 'EACCES') return callback(new BoxError(BoxError.BAD_FIELD, `Access denied. Run "chown yellowtent:yellowtent ${backupPath}" on the server`, { field }));
|
||||
return callback(new BoxError(BoxError.BAD_FIELD, safe.error.message, { field }));
|
||||
}
|
||||
if (!safe.fs.writeFileSync(path.join(backupPath, 'cloudron-testfile'), 'testcontent')) {
|
||||
return callback(new BoxError(BoxError.BAD_FIELD, `Unable to create test file as 'yellowtent' user in ${backupPath}: ${safe.error.message}. Check dir/mount permissions`, { field }));
|
||||
}
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(backupPath, 'cloudron-testfile'), 'testcontent')) {
|
||||
return callback(new BoxError(BoxError.BAD_FIELD, `Unable to create test file as 'yellowtent' user in ${backupPath}: ${safe.error.message}. Check dir/mount permissions`, { field }));
|
||||
}
|
||||
|
||||
if (!safe.fs.unlinkSync(path.join(backupPath, 'cloudron-testfile'))) {
|
||||
return callback(new BoxError(BoxError.BAD_FIELD, `Unable to remove test file as 'yellowtent' user in ${backupPath}: ${safe.error.message}. Check dir/mount permissions`, { field }));
|
||||
}
|
||||
if (!safe.fs.unlinkSync(path.join(backupPath, 'cloudron-testfile'))) {
|
||||
return callback(new BoxError(BoxError.BAD_FIELD, `Unable to remove test file as 'yellowtent' user in ${backupPath}: ${safe.error.message}. Check dir/mount permissions`, { field }));
|
||||
}
|
||||
|
||||
callback(null);
|
||||
|
||||
Reference in New Issue
Block a user