2020-10-27 22:39:05 -07:00
'use strict' ;
exports = module . exports = {
add ,
get ,
del ,
2021-05-12 18:00:43 -07:00
list ,
2021-05-13 15:33:16 -07:00
getStatus ,
2021-06-21 16:35:08 -07:00
removePrivateFields
2020-10-27 22:39:05 -07:00
} ;
const assert = require ( 'assert' ) ,
BoxError = require ( './boxerror.js' ) ,
2021-01-04 11:05:42 -08:00
collectd = require ( './collectd.js' ) ,
2021-05-17 16:23:24 -07:00
constants = require ( './constants.js' ) ,
2021-05-11 17:50:48 -07:00
database = require ( './database.js' ) ,
2020-10-30 11:07:24 -07:00
debug = require ( 'debug' ) ( 'box:volumes' ) ,
2021-01-04 11:05:42 -08:00
ejs = require ( 'ejs' ) ,
2020-10-27 22:39:05 -07:00
eventlog = require ( './eventlog.js' ) ,
2021-01-04 11:05:42 -08:00
fs = require ( 'fs' ) ,
2021-05-14 15:07:29 -07:00
mounts = require ( './mounts.js' ) ,
2020-12-03 23:05:06 -08:00
path = require ( 'path' ) ,
2021-06-24 16:59:47 -07:00
paths = require ( './paths.js' ) ,
2020-12-03 23:13:20 -08:00
safe = require ( 'safetydance' ) ,
2021-01-21 12:53:38 -08:00
services = require ( './services.js' ) ,
2021-05-11 17:50:48 -07:00
uuid = require ( 'uuid' ) ;
2021-05-12 18:00:43 -07:00
const VOLUMES _FIELDS = [ 'id' , 'name' , 'hostPath' , 'creationTime' , 'mountType' , 'mountOptionsJson' ] . join ( ',' ) ;
2020-10-27 22:39:05 -07:00
2021-01-04 11:05:42 -08:00
const COLLECTD _CONFIG _EJS = fs . readFileSync ( _ _dirname + '/collectd/volume.ejs' , { encoding : 'utf8' } ) ;
2021-05-12 18:00:43 -07:00
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
result . mountOptions = safe . JSON . parse ( result . mountOptionsJson ) || { } ;
delete result . mountOptionsJson ;
return result ;
}
2020-10-27 22:39:05 -07:00
function validateName ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
if ( ! /^[-\w^&'@{}[\],$=!#().%+~ ]+$/ . test ( name ) ) return new BoxError ( BoxError . BAD _FIELD , 'Invalid name' ) ;
return null ;
}
2021-05-12 18:00:43 -07:00
function validateHostPath ( hostPath , mountType ) {
2020-10-27 22:39:05 -07:00
assert . strictEqual ( typeof hostPath , 'string' ) ;
2021-05-12 18:00:43 -07:00
assert . strictEqual ( typeof mountType , 'string' ) ;
2020-10-27 22:39:05 -07:00
2020-12-03 23:05:06 -08:00
if ( path . normalize ( hostPath ) !== hostPath ) return new BoxError ( BoxError . BAD _FIELD , 'hostPath must contain a normalized path' , { field : 'hostPath' } ) ;
if ( ! path . isAbsolute ( hostPath ) ) return new BoxError ( BoxError . BAD _FIELD , 'backupFolder must be an absolute path' , { field : 'hostPath' } ) ;
if ( hostPath === '/' ) return new BoxError ( BoxError . BAD _FIELD , 'hostPath cannot be /' , { field : 'hostPath' } ) ;
if ( ! hostPath . endsWith ( '/' ) ) hostPath = hostPath + '/' ; // ensure trailing slash for the prefix matching to work
const allowedPaths = [ '/mnt/' , '/media/' , '/srv/' , '/opt/' ] ;
if ( ! allowedPaths . some ( p => hostPath . startsWith ( p ) ) ) return new BoxError ( BoxError . BAD _FIELD , 'hostPath must be under /mnt, /media, /opt or /srv' , { field : 'hostPath' } ) ;
2020-10-27 22:39:05 -07:00
2021-06-24 23:01:18 -07:00
if ( ! constants . TEST ) { // we expect user to have already mounted this
2021-05-12 18:00:43 -07:00
const stat = safe . fs . lstatSync ( hostPath ) ;
2021-06-18 23:31:11 -07:00
if ( ! stat ) return new BoxError ( BoxError . BAD _FIELD , 'mount point does not exist. Please create it on the server first' , { field : 'hostPath' } ) ;
if ( ! stat . isDirectory ( ) ) return new BoxError ( BoxError . BAD _FIELD , 'mount point is not a directory' , { field : 'hostPath' } ) ;
2021-05-12 18:00:43 -07:00
}
2020-12-03 23:13:20 -08:00
2020-10-27 22:39:05 -07:00
return null ;
}
2021-05-12 18:00:43 -07:00
async function add ( volume , auditSource ) {
assert . strictEqual ( typeof volume , 'object' ) ;
2020-10-27 22:39:05 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-06-24 16:59:47 -07:00
const { name , mountType , mountOptions } = volume ;
2021-05-12 18:00:43 -07:00
2020-10-27 22:39:05 -07:00
let error = validateName ( name ) ;
2021-05-11 17:50:48 -07:00
if ( error ) throw error ;
2020-10-27 22:39:05 -07:00
2021-05-14 15:07:29 -07:00
error = mounts . validateMountOptions ( mountType , mountOptions ) ;
2021-05-11 17:50:48 -07:00
if ( error ) throw error ;
2020-10-27 22:39:05 -07:00
2021-06-24 16:59:47 -07:00
const id = uuid . v4 ( ) . replace ( /-/g , '' ) ; // to make systemd mount file names more readable
2021-06-24 23:01:18 -07:00
if ( mountType === 'mountpoint' || mountType === 'filesystem' ) {
2021-06-24 16:59:47 -07:00
error = validateHostPath ( volume . hostPath , mountType ) ;
if ( error ) throw error ;
} else {
volume . hostPath = path . join ( paths . VOLUMES _MOUNT _DIR , id ) ;
2021-06-24 23:01:18 -07:00
await mounts . tryAddMount ( volume , { timeout : 10 } ) ; // 10 seconds
2021-06-24 16:59:47 -07:00
}
2020-10-27 22:39:05 -07:00
2021-09-01 15:29:35 -07:00
[ error ] = await safe ( database . query ( 'INSERT INTO volumes (id, name, hostPath, mountType, mountOptionsJson) VALUES (?, ?, ?, ?, ?)' , [ id , name , volume . hostPath , mountType , JSON . stringify ( mountOptions ) ] ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'name' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'name already exists' ) ;
if ( error && error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'hostPath' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'hostPath already exists' ) ;
if ( error && error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'PRIMARY' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'id already exists' ) ;
if ( error ) throw error ;
2020-10-27 22:39:05 -07:00
2021-06-24 16:59:47 -07:00
eventlog . add ( eventlog . ACTION _VOLUME _ADD , auditSource , { id , name , hostPath : volume . hostPath } ) ;
2021-06-24 23:01:18 -07:00
// in theory, we only need to do this mountpoint volumes. but for some reason a restart is required to detect new "mounts"
2021-09-17 09:22:46 -07:00
safe ( services . rebuildService ( 'sftp' ) , { debug } ) ;
2021-01-04 11:05:42 -08:00
2021-06-24 16:59:47 -07:00
const collectdConf = ejs . render ( COLLECTD _CONFIG _EJS , { volumeId : id , hostPath : volume . hostPath } ) ;
2021-08-19 13:24:38 -07:00
await collectd . addProfile ( id , collectdConf ) ;
2020-10-27 22:39:05 -07:00
2021-05-11 17:50:48 -07:00
return id ;
2020-10-27 22:39:05 -07:00
}
2021-05-19 09:50:50 -07:00
async function getStatus ( volume ) {
assert . strictEqual ( typeof volume , 'object' ) ;
return await mounts . getStatus ( volume . mountType , volume . hostPath ) ; // { state, message }
}
2021-06-21 16:35:08 -07:00
function removePrivateFields ( volume ) {
const newVolume = Object . assign ( { } , volume ) ;
if ( newVolume . mountType === 'sshfs' ) {
newVolume . mountOptions . privateKey = constants . SECRET _PLACEHOLDER ;
} else if ( newVolume . mountType === 'cifs' ) {
newVolume . mountOptions . password = constants . SECRET _PLACEHOLDER ;
}
return newVolume ;
}
2021-05-11 17:50:48 -07:00
async function get ( id ) {
2020-10-27 22:39:05 -07:00
assert . strictEqual ( typeof id , 'string' ) ;
2021-05-11 17:50:48 -07:00
const result = await database . query ( ` SELECT ${ VOLUMES _FIELDS } FROM volumes WHERE id=? ` , [ id ] ) ;
if ( result . length === 0 ) return null ;
2020-10-27 22:39:05 -07:00
2021-05-12 18:00:43 -07:00
return postProcess ( result [ 0 ] ) ;
2020-10-27 22:39:05 -07:00
}
2021-05-11 17:50:48 -07:00
async function list ( ) {
2021-05-12 18:00:43 -07:00
const results = await database . query ( ` SELECT ${ VOLUMES _FIELDS } FROM volumes ORDER BY name ` ) ;
results . forEach ( postProcess ) ;
return results ;
2020-10-27 22:39:05 -07:00
}
2021-05-11 17:50:48 -07:00
async function del ( volume , auditSource ) {
2020-10-28 15:51:43 -07:00
assert . strictEqual ( typeof volume , 'object' ) ;
2020-10-27 22:39:05 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-08-19 13:24:38 -07:00
const [ error , result ] = await safe ( database . query ( 'DELETE FROM volumes WHERE id=?' , [ volume . id ] ) ) ;
if ( error && error . code === 'ER_ROW_IS_REFERENCED_2' ) throw new BoxError ( BoxError . CONFLICT , 'Volume is in use' ) ;
if ( error ) throw error ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Volume not found' ) ;
2021-05-11 17:50:48 -07:00
eventlog . add ( eventlog . ACTION _VOLUME _REMOVE , auditSource , { volume } ) ;
2021-06-24 16:19:30 -07:00
2021-06-24 23:01:18 -07:00
if ( volume . mountType === 'mountpoint' || volume . mountType === 'filesystem' ) {
2021-09-17 09:22:46 -07:00
safe ( services . rebuildService ( 'sftp' ) , { debug } ) ;
2021-06-24 16:19:30 -07:00
} else {
await safe ( mounts . removeMount ( volume ) ) ;
}
2021-08-19 13:24:38 -07:00
await collectd . removeProfile ( volume . id ) ;
2020-10-27 22:39:05 -07:00
}