diff --git a/setup/start/sudoers b/setup/start/sudoers index fc441daf9..259baa91b 100644 --- a/setup/start/sudoers +++ b/setup/start/sudoers @@ -59,3 +59,5 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/addmount.sh Defaults!/home/yellowtent/box/src/scripts/rmmount.sh env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmount.sh +Defaults!/home/yellowtent/box/src/scripts/remountmount.sh env_keep="HOME BOX_ENV" +yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remountmount.sh diff --git a/src/boxerror.js b/src/boxerror.js index 238629e77..e6bebb58e 100644 --- a/src/boxerror.js +++ b/src/boxerror.js @@ -60,6 +60,7 @@ BoxError.NGINX_ERROR = 'Nginx Error'; BoxError.NOT_FOUND = 'Not found'; BoxError.NOT_IMPLEMENTED = 'Not implemented'; BoxError.NOT_SIGNED = 'Not Signed'; +BoxError.NOT_SUPPORTED = 'Not Supported'; BoxError.OPENSSL_ERROR = 'OpenSSL Error'; BoxError.PLAN_LIMIT = 'Plan Limit'; BoxError.SPAWN_ERROR = 'Spawn Error'; @@ -85,6 +86,7 @@ BoxError.toHttpError = function (error) { case BoxError.ALREADY_EXISTS: case BoxError.BAD_STATE: case BoxError.CONFLICT: + case BoxError.NOT_SUPPORTED: return new HttpError(409, error); case BoxError.INVALID_CREDENTIALS: return new HttpError(412, error); diff --git a/src/eventlog.js b/src/eventlog.js index 7cd72c8f0..726efb49d 100644 --- a/src/eventlog.js +++ b/src/eventlog.js @@ -73,6 +73,7 @@ exports = module.exports = { ACTION_VOLUME_ADD: 'volume.add', ACTION_VOLUME_UPDATE: 'volume.update', + ACTION_VOLUME_REMOUNT: 'volume.remount', ACTION_VOLUME_REMOVE: 'volume.remove', ACTION_DYNDNS_UPDATE: 'dyndns.update', diff --git a/src/mounts.js b/src/mounts.js index a64d9e70a..9836a6af3 100644 --- a/src/mounts.js +++ b/src/mounts.js @@ -6,6 +6,7 @@ exports = module.exports = { removeMount, validateMountOptions, getStatus, + remount }; const assert = require('assert'), @@ -21,6 +22,7 @@ const assert = require('assert'), const ADD_MOUNT_CMD = path.join(__dirname, 'scripts/addmount.sh'); const RM_MOUNT_CMD = path.join(__dirname, 'scripts/rmmount.sh'); +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 @@ -184,3 +186,14 @@ async function tryAddMount(mount, options) { throw new BoxError(BoxError.MOUNT_ERROR, `Failed to mount (${status.state}): ${status.message}`); } } + +async function remount(mount) { + assert.strictEqual(typeof mount, 'object'); // { name, hostPath, mountType, mountOptions } + + if (mount.mountType === 'mountpoint') return; + + if (constants.TEST) return; + + const [error] = await safe(shell.promises.sudo('remountMount', [ REMOUNT_MOUNT_CMD, mount.hostPath ], {})); + if (error && error.code === 2) throw new BoxError(BoxError.MOUNT_ERROR, 'Failed to remount existing mount'); // at this point, the old mount config is still there +} diff --git a/src/routes/volumes.js b/src/routes/volumes.js index daac50319..4833a54da 100644 --- a/src/routes/volumes.js +++ b/src/routes/volumes.js @@ -6,6 +6,7 @@ exports = module.exports = { del, list, load, + remount, getStatus }; @@ -66,6 +67,14 @@ async function list(req, res, next) { next(new HttpSuccess(200, { volumes: allVolumes })); } +async function remount(req, res, next) { + assert.strictEqual(typeof req.params.id, 'string'); + + const [error] = await safe(volumes.remount(req.volume, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + next(new HttpSuccess(202)); +} + async function getStatus(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); diff --git a/src/scripts/remountmount.sh b/src/scripts/remountmount.sh new file mode 100755 index 000000000..bc5ea89bc --- /dev/null +++ b/src/scripts/remountmount.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -eu -o pipefail + +if [[ ${EUID} -ne 0 ]]; then + echo "This script should be run as root." > /dev/stderr + exit 1 +fi + +if [[ $# -eq 0 ]]; then + echo "No arguments supplied" + exit 1 +fi + +if [[ "$1" == "--check" ]]; then + echo "OK" + exit 0 +fi + +host_path="$1" + +# mount units must be named after the mount point directories they control +mount_filename=$(systemd-escape -p --suffix=mount "$host_path") +mount_file="/etc/systemd/system/${mount_filename}" + +# stop and start will do the reumount +systemctl stop "${mount_filename}" +systemctl start "${mount_filename}" + +echo "Remount succeeded" diff --git a/src/server.js b/src/server.js index 0911e3793..10a4b4421 100644 --- a/src/server.js +++ b/src/server.js @@ -320,6 +320,7 @@ function initializeExpressSync() { router.get ('/api/v1/volumes/:id', token, authorizeAdmin, routes.volumes.load, routes.volumes.get); router.del ('/api/v1/volumes/:id', token, authorizeAdmin, routes.volumes.load, routes.volumes.del); router.get ('/api/v1/volumes/:id/status', token, authorizeAdmin, routes.volumes.load, routes.volumes.getStatus); + router.post('/api/v1/volumes/:id/remount', token, authorizeAdmin, routes.volumes.load, routes.volumes.remount); router.use ('/api/v1/volumes/:id/files/*', token, authorizeAdmin, routes.filemanager.proxy('volume')); // service routes diff --git a/src/volumes.js b/src/volumes.js index 813153ba3..882acf1b5 100644 --- a/src/volumes.js +++ b/src/volumes.js @@ -5,6 +5,7 @@ exports = module.exports = { get, del, list, + remount, getStatus, removePrivateFields, @@ -109,6 +110,17 @@ async function add(volume, auditSource) { return id; } +async function remount(volume, auditSource) { + assert.strictEqual(typeof volume, 'object'); + assert.strictEqual(typeof auditSource, 'object'); + + eventlog.add(eventlog.ACTION_VOLUME_REMOUNT, auditSource, { volume }); + + if (!mounts.isMountProvider(volume.mountType)) throw new BoxError(BoxError.NOT_SUPPORTED, 'Volume does not support remount'); + + mounts.remount(volume); +} + async function getStatus(volume) { assert.strictEqual(typeof volume, 'object');