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 09:14:50 -07:00
update ,
2021-05-12 18:00:43 -07:00
getMountStatus ,
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-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' ) ,
2020-12-03 23:05:06 -08:00
path = require ( 'path' ) ,
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-12 18:00:43 -07:00
shell = require ( './shell.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 ( ',' ) ;
const ADD _MOUNT _CMD = path . join ( _ _dirname , 'scripts/addmount.sh' ) ;
const RM _MOUNT _CMD = path . join ( _ _dirname , 'scripts/rmmount.sh' ) ;
const SYSTEMD _MOUNT _EJS = fs . readFileSync ( path . join ( _ _dirname , 'systemd-mount.ejs' ) , { encoding : 'utf8' } ) ;
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' } ) ;
const NOOP _CALLBACK = function ( error ) { if ( error ) debug ( error ) ; } ;
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
// https://man7.org/linux/man-pages/man8/mount.8.html
function validateMountOptions ( type , options ) {
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
switch ( type ) {
case 'noop' :
return null ;
case 'cifs' :
if ( typeof options . username !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'username is not a string' ) ;
if ( typeof options . password !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'password is not a string' ) ;
if ( typeof options . host !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'host is not a string' ) ;
if ( typeof options . remoteDir !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'remoteDir is not a string' ) ;
return null ;
case 'nfs' :
if ( typeof options . host !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'host is not a string' ) ;
if ( typeof options . remoteDir !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'remoteDir is not a string' ) ;
return null ;
case 'ext4' :
if ( typeof options . diskPath !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'diskPath is not a string' ) ;
return null ;
default :
return new BoxError ( BoxError . BAD _FIELD , 'Bad volume mount type' ) ;
}
}
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-05-12 18:00:43 -07:00
if ( mountType === 'noop' ) { // we expect user to have already mounted this
const stat = safe . fs . lstatSync ( hostPath ) ;
if ( ! stat ) return new BoxError ( BoxError . BAD _FIELD , 'hostPath does not exist. Please create it on the server first' , { field : 'hostPath' } ) ;
if ( ! stat . isDirectory ( ) ) return new BoxError ( BoxError . BAD _FIELD , 'hostPath is not a directory' , { field : 'hostPath' } ) ;
}
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 writeMountFile ( volume ) {
assert . strictEqual ( typeof volume , 'object' ) ;
const { name , hostPath , mountType , mountOptions } = volume ;
let options , what , type ;
switch ( mountType ) {
case 'cifs' :
type = 'cifs' ;
what = ` ${ mountOptions . host } : ${ mountOptions . remoteDir } ` ;
options = ` username= ${ mountOptions . username } ,password= ${ mountOptions . password } ,rw ` ; // uid=1000 ?
break ;
case 'nfs' :
type = 'nfs' ;
what = ` ${ mountOptions . host } : ${ mountOptions . remoteDir } ` ;
options = 'noauto,x-systemd.automount' ; // _netdev is implicit
break ;
case 'ext4' :
type = 'ext4' ;
what = mountOptions . diskPath ; // like /dev/disk/by-uuid/uuid
options = 'defaults' ;
break ;
case 'sshfs' :
// type = 'sshfs';
// What={{ USER }}@{{ HOST }}:{{ REMOTE DIR }}
// Options=_netdev,allow_other,IdentityFile=/home/{{ MY LOCAL USER WITH SSH KEY IN ITS HOME DIRECTORY }}/.ssh/id_rsa,reconnect,x-systemd.automount,uid=1000,gid=1000
}
const mountFileContents = ejs . render ( SYSTEMD _MOUNT _EJS , { name , what , where : hostPath , options , type } ) ;
const [ error ] = await safe ( shell . promises . sudo ( 'generateMountFile' , [ ADD _MOUNT _CMD , mountFileContents ] , { } ) ) ;
if ( error ) throw error ;
}
async function removeMountFile ( volume ) {
assert . strictEqual ( typeof volume , 'object' ) ;
await safe ( shell . promises . sudo ( 'generateMountFile' , [ RM _MOUNT _CMD , volume . hostPath ] , { } ) ) ; // ignore any error
}
2021-05-13 09:14:50 -07:00
async function update ( volume , mountType , mountOptions ) {
2021-05-12 18:00:43 -07:00
assert . strictEqual ( typeof volume , 'object' ) ;
assert . strictEqual ( typeof mountType , 'string' ) ;
assert . strictEqual ( typeof mountOptions , 'object' ) ;
let error = validateMountOptions ( mountType , mountOptions ) ;
if ( error ) throw error ;
2021-05-13 09:14:50 -07:00
if ( mountType === 'noop' ) {
await safe ( removeMountFile ( Object . assign ( { } , volume , { mountType , mountOptions } ) ) ) ;
} else {
await safe ( writeMountFile ( Object . assign ( { } , volume , { mountType , mountOptions } ) ) ) ;
}
2021-05-12 18:00:43 -07:00
let result ;
2021-05-13 09:14:50 -07:00
[ error , result ] = await safe ( database . query ( 'UPDATE volumes SET mountType=?, mountOptionsJson=? WHERE id=?' , [ mountType , JSON . stringify ( mountOptions ) , volume . id ] ) ) ;
2021-05-12 18:00:43 -07:00
if ( error ) throw error ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Volume not found' ) ;
}
async function getMountStatus ( volume ) {
assert . strictEqual ( typeof volume , 'object' ) ;
let [ error , activeState ] = await safe ( shell . promises . exec ( 'getMountStatus' , ` systemctl is-active $ (systemd-escape -p ${ volume . hostPath } ) ` ) ) ;
activeState = activeState || 'no status' ;
return { activeState } ;
}
async function add ( volume , auditSource ) {
assert . strictEqual ( typeof volume , 'object' ) ;
2020-10-27 22:39:05 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-05-12 18:00:43 -07:00
const { name , hostPath , mountType , mountOptions } = volume ;
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-12 18:00:43 -07:00
error = validateHostPath ( hostPath , mountType ) ;
if ( error ) throw error ;
error = validateMountOptions ( mountType , mountOptions ) ;
2021-05-11 17:50:48 -07:00
if ( error ) throw error ;
2020-10-27 22:39:05 -07:00
2021-02-17 23:14:47 -08:00
const id = uuid . v4 ( ) ;
2020-10-27 22:39:05 -07:00
2021-05-11 17:50:48 -07:00
try {
2021-05-12 18:00:43 -07:00
if ( mountType !== 'noop' ) await writeMountFile ( volume ) ;
await database . query ( 'INSERT INTO volumes (id, name, hostPath, mountType, mountOptionsJson) VALUES (?, ?, ?, ?, ?)' , [ id , name , hostPath , mountType , JSON . stringify ( mountOptions ) ] ) ;
2021-05-11 17:50:48 -07:00
} catch ( error ) {
if ( error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'name' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'name already exists' ) ;
if ( error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'hostPath' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'hostPath already exists' ) ;
if ( error . code === 'ER_DUP_ENTRY' && error . sqlMessage . indexOf ( 'PRIMARY' ) !== - 1 ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'id already exists' ) ;
throw error ;
}
2020-10-27 22:39:05 -07:00
2021-05-11 17:50:48 -07:00
eventlog . add ( eventlog . ACTION _VOLUME _ADD , auditSource , { id , name , hostPath } ) ;
services . rebuildService ( 'sftp' , NOOP _CALLBACK ) ;
2021-01-04 11:05:42 -08:00
2021-05-11 17:50:48 -07:00
const collectdConf = ejs . render ( COLLECTD _CONFIG _EJS , { volumeId : id , hostPath } ) ;
collectd . addProfile ( id , collectdConf , NOOP _CALLBACK ) ;
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-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-05-11 17:50:48 -07:00
try {
const result = await database . query ( 'DELETE FROM volumes WHERE id=?' , [ volume . id ] ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Volume not found' ) ;
} catch ( error ) {
if ( error . code === 'ER_ROW_IS_REFERENCED_2' ) throw new BoxError ( BoxError . CONFLICT , 'Volume is in use' ) ;
throw error ;
}
eventlog . add ( eventlog . ACTION _VOLUME _REMOVE , auditSource , { volume } ) ;
2021-05-12 18:00:43 -07:00
services . rebuildService ( 'sftp' , async function ( ) {
await safe ( removeMountFile ( volume ) ) ;
} ) ;
2021-05-11 17:50:48 -07:00
collectd . removeProfile ( volume . id , NOOP _CALLBACK ) ;
2020-10-27 22:39:05 -07:00
}