diff --git a/dashboard/src/components/BackupProviderForm.vue b/dashboard/src/components/BackupProviderForm.vue index b01b3b014..6f4555f40 100644 --- a/dashboard/src/components/BackupProviderForm.vue +++ b/dashboard/src/components/BackupProviderForm.vue @@ -27,8 +27,8 @@ const systemModel = SystemModel.create(); const provisionModel = ProvisionModel.create(); const storageProviders = Array.from(STORAGE_PROVIDERS); -const blockDevices = ref([]); -const disk = ref(''); +const ext4BlockDevices = ref([]); +const xfsBlockDevices = ref([]); const gcsKeyFileName = ref(''); const gcsFileParseError = ref(''); @@ -67,28 +67,21 @@ function onGcsKeyChange(event) { } async function getBlockDevices() { - let error, result; + let error, blockDevices; - if (props.provisioning) [error, result] = await provisionModel.blockDevices(); - else [error, result] = await systemModel.blockDevices(); + if (props.provisioning) [error, blockDevices] = await provisionModel.blockDevices(); + else [error, blockDevices] = await systemModel.blockDevices(); if (error) return console.error(error); - // amend label for UI - result.forEach(d => { - d.label = d.path; - - // pre-select current if set - if (d.path === providerConfig.value.mountOptionDiskPath || ('/dev/disk/by-uuid/' + d.uuid) === providerConfig.value.mountOptionDiskPath) { - disk.value = d.path; - } - }); - - // only offer non /, /boot or /home disks - // only offer xfs and ext4 disks - blockDevices.value = result - .filter(d => { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; }) - .filter(d => { return d.type === 'xfs' || d.type === 'ext4'; }); + ext4BlockDevices.value = []; + xfsBlockDevices.value = []; + for (const blockDevice of blockDevices) { + if (blockDevice.mountpoints.some((mountpoint) => mountpoint === '/' || mountpoint.startsWith('/home') || mountpoint.startsWith('/boot'))) continue; + blockDevice.label = blockDevice.path; // // amend label for UI + if (blockDevice.type === 'ext4') ext4BlockDevices.value.push(blockDevice); + else if (blockDevice.type === 'xfs') xfsBlockDevices.value.push(blockDevice); + } } watch(provider, (newProvider) => { @@ -153,14 +146,15 @@ onMounted(async () => { - - + + + - + - + diff --git a/dashboard/src/views/VolumesView.vue b/dashboard/src/views/VolumesView.vue index 8b3d97f02..290a82f88 100644 --- a/dashboard/src/views/VolumesView.vue +++ b/dashboard/src/views/VolumesView.vue @@ -163,6 +163,7 @@ async function openVolumeDialog(volume) { const ext4BlockDevices = [], xfsBlockDevices = []; for (const blockDevice of blockDevices) { + if (blockDevice.mountpoints.some((mountpoint) => mountpoint === '/' || mountpoint.startsWith('/home') || mountpoint.startsWith('/boot'))) continue; blockDevice.label = blockDevice.path; // // amend label for UI if (blockDevice.type === 'ext4') ext4BlockDevices.push(blockDevice); else if (blockDevice.type === 'xfs') xfsBlockDevices.push(blockDevice); @@ -295,8 +296,8 @@ onMounted(async () =>{ - - + + diff --git a/src/backupsites.js b/src/backupsites.js index 5150490c5..9ca2797cc 100644 --- a/src/backupsites.js +++ b/src/backupsites.js @@ -76,6 +76,7 @@ function storageApi(backupSite) { case 'mountpoint': return require('./storage/filesystem.js'); case 'disk': return require('./storage/filesystem.js'); case 'ext4': return require('./storage/filesystem.js'); + case 'xfs': return require('./storage/filesystem.js'); case 's3': return require('./storage/s3.js'); case 'gcs': return require('./storage/gcs.js'); case 'filesystem': return require('./storage/filesystem.js'); diff --git a/src/mounts.js b/src/mounts.js index 4bcbc1555..68c1acadb 100644 --- a/src/mounts.js +++ b/src/mounts.js @@ -13,9 +13,9 @@ exports = module.exports = { MOUNT_TYPE_CIFS: 'cifs', MOUNT_TYPE_NFS: 'nfs', MOUNT_TYPE_SSHFS: 'sshfs', - MOUNT_TYPE_EXT4: 'ext4', - MOUNT_TYPE_XFS: 'xfs', - MOUNT_TYPE_DISK: 'disk', + MOUNT_TYPE_EXT4: 'ext4', // raw disk path + MOUNT_TYPE_XFS: 'xfs', // raw disk path + MOUNT_TYPE_DISK: 'disk', // this provides a selector of block devices MOUNT_TYPE_LOOPBACK: 'loopback' }; @@ -36,7 +36,7 @@ const REMOUNT_MOUNT_CMD = path.join(__dirname, 'scripts/remountmount.sh'); const SYSTEMD_MOUNT_EJS = fs.readFileSync(path.join(__dirname, 'systemd-mount.ejs'), { encoding: 'utf8' }); // https://man7.org/linux/man-pages/man8/mount.8.html -function validateMountOptions(type, options) { +async function validateMountOptions(type, options) { assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof options, 'object'); @@ -66,10 +66,18 @@ function validateMountOptions(type, options) { case exports.MOUNT_TYPE_EXT4: case exports.MOUNT_TYPE_XFS: case exports.MOUNT_TYPE_DISK: - case exports.MOUNT_TYPE_LOOPBACK: + case exports.MOUNT_TYPE_LOOPBACK: { if (typeof options.diskPath !== 'string') return new BoxError(BoxError.BAD_FIELD, 'diskPath is not a string'); - // TODO: check if this diskPath is not mounted on '/' or somewhere dangerous + const [error, output] = await safe(shell.spawn('lsblk', ['--paths', '--json', '--list', '--fs', options.diskPath ], { encoding: 'utf8' })); + if (error) return new BoxError(BoxError.BAD_FIELD, `Bad disk path: ${error.message}`); + const info = safe.JSON.parse(output); + if (!info) return new BoxError(BoxError.BAD_FIELD, `Bad disk path: ${safe.error.message}`); + for (const mountpoint of info.blockdevices[0].mountpoints) { + if (mountpoint === null) break; // [ null ] means not mounted anywhere + if (mountpoint === '/' || mountpoint.startsWith('/home') || mountpoint.startsWith('/boot')) return new BoxError(BoxError.BAD_FIELD, 'Disk is mounted in a protected location'); + } return null; + } default: return new BoxError(BoxError.BAD_FIELD, 'Bad mount type'); } diff --git a/src/storage/filesystem.js b/src/storage/filesystem.js index 836888d7c..0ec94b580 100644 --- a/src/storage/filesystem.js +++ b/src/storage/filesystem.js @@ -352,7 +352,7 @@ async function verifyConfig({ id, provider, config }) { if (mounts.isManagedProvider(provider)) { if (!config.mountOptions || typeof config.mountOptions !== 'object') throw new BoxError(BoxError.BAD_FIELD, 'mountOptions must be an object'); - const error = mounts.validateMountOptions(provider, config.mountOptions); + const error = await mounts.validateMountOptions(provider, config.mountOptions); if (error) throw error; const tmpConfig = { diff --git a/src/system.js b/src/system.js index ea14de0ed..0f5943b9e 100644 --- a/src/system.js +++ b/src/system.js @@ -327,13 +327,14 @@ async function getBlockDevices() { const result = []; for (const device of devices) { - const mountpoints = Array.isArray(device.mountpoints) ? device.mountpoints : [ device.mountpoint ]; // we only support one mountpoint here old lsblk only exposed one via .mountpoint - if (mountpoints.includes('/') || mountpoints.includes('/boot')) continue; // cannot be used for backups and volumes + const mountpoints = Array.isArray(device.mountpoints) + ? (device.mountpoints[0] === null ? [] : device.mountpoints) // convert [ null ] to [] + : (device.mountpoint ? [ device.mountpoint ] : []); // old lsblk only exposed one .mountpoint result.push({ path: device.name, size: device.fsavail || 0, - type: device.fstype, + type: device.fstype, // when null, it is not formatted uuid: device.uuid, rota: device.rota, // false (ssd) true (hdd) . unforuntately, this is not set correctly when virtualized (like in DO) mountpoints diff --git a/src/volumes.js b/src/volumes.js index e5e595f8f..049936884 100644 --- a/src/volumes.js +++ b/src/volumes.js @@ -80,7 +80,7 @@ async function add(volume, auditSource) { let error = validateName(name); if (error) throw error; - error = mounts.validateMountOptions(mountType, mountOptions); + error = await mounts.validateMountOptions(mountType, mountOptions); if (error) throw error; const id = crypto.randomUUID().replace(/-/g, ''); // to make systemd mount file names more readable @@ -149,7 +149,7 @@ async function update(id, mountOptions, auditSource) { if (mountType !== mounts.MOUNT_TYPE_CIFS && mountType !== mounts.MOUNT_TYPE_SSHFS && mountType !== mounts.MOUNT_TYPE_NFS) throw new BoxError(BoxError.BAD_FIELD, 'Only network mounts can be updated'); - const error = mounts.validateMountOptions(mountType, mountOptions); + const error = await mounts.validateMountOptions(mountType, mountOptions); if (error) throw error; // put old secret back in place if no new secret is provided