2021-05-14 15:07:29 -07:00
'use strict' ;
exports = module . exports = {
2021-07-14 11:07:19 -07:00
isMountProvider ,
2021-05-21 17:31:54 -07:00
tryAddMount ,
2021-06-21 12:11:05 -07:00
removeMount ,
2021-05-14 15:07:29 -07:00
validateMountOptions ,
2021-05-19 09:50:50 -07:00
getStatus ,
2021-05-14 15:07:29 -07:00
} ;
const assert = require ( 'assert' ) ,
BoxError = require ( './boxerror.js' ) ,
2021-05-17 16:23:24 -07:00
constants = require ( './constants.js' ) ,
2021-09-29 20:15:54 -07:00
debug = require ( 'debug' ) ( 'box:mounts' ) ,
2021-05-14 15:07:29 -07:00
ejs = require ( 'ejs' ) ,
fs = require ( 'fs' ) ,
path = require ( 'path' ) ,
2021-05-18 16:49:39 +02:00
paths = require ( './paths.js' ) ,
2021-05-14 15:07:29 -07:00
safe = require ( 'safetydance' ) ,
shell = require ( './shell.js' ) ;
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' } ) ;
// 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 ) {
2021-06-24 23:01:18 -07:00
case 'filesystem' :
case 'mountpoint' :
2021-05-14 15:07:29 -07:00
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 ;
2021-05-18 16:49:39 +02:00
case 'sshfs' :
if ( typeof options . user !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'user is not a string' ) ;
if ( typeof options . privateKey !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'privateKey is not a string' ) ;
if ( typeof options . port !== 'number' ) return new BoxError ( BoxError . BAD _FIELD , 'port is not a number' ) ;
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 ;
2021-05-14 15:07:29 -07:00
case 'ext4' :
if ( typeof options . diskPath !== 'string' ) return new BoxError ( BoxError . BAD _FIELD , 'diskPath is not a string' ) ;
return null ;
default :
2021-09-28 11:43:15 -07:00
return new BoxError ( BoxError . BAD _FIELD , 'Bad mount type' ) ;
2021-05-14 15:07:29 -07:00
}
}
2021-07-14 11:07:19 -07:00
function isMountProvider ( provider ) {
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'ext4' ;
}
2021-06-21 12:11:05 -07:00
// https://www.man7.org/linux/man-pages/man8/mount.8.html for various mount option flags
// nfs - no_root_squash is mode on server to map all root to 'nobody' user. all_squash does this for all users (making it like ftp)
// sshfs - supports users/permissions
// cifs - does not support permissions
2021-09-28 11:43:15 -07:00
function renderMountFile ( mount ) {
assert . strictEqual ( typeof mount , 'object' ) ;
2021-05-14 15:07:29 -07:00
2021-09-28 11:43:15 -07:00
const { name , hostPath , mountType , mountOptions } = mount ;
2021-05-14 15:07:29 -07:00
let options , what , type ;
switch ( mountType ) {
case 'cifs' :
type = 'cifs' ;
what = ` // ${ mountOptions . host } ` + path . join ( '/' , mountOptions . remoteDir ) ;
2021-06-20 22:36:23 -07:00
options = ` username= ${ mountOptions . username } ,password= ${ mountOptions . password } ,rw,iocharset=utf8,file_mode=0666,dir_mode=0777,uid=yellowtent,gid=yellowtent ` ;
2021-05-14 15:07:29 -07:00
break ;
case 'nfs' :
type = 'nfs' ;
what = ` ${ mountOptions . host } : ${ mountOptions . remoteDir } ` ;
2021-06-18 23:48:39 -07:00
options = 'noauto' ; // noauto means it is not a blocker for local-fs.target. _netdev is implicit. rw,hard,tcp,rsize=8192,wsize=8192,timeo=14
2021-05-14 15:07:29 -07:00
break ;
case 'ext4' :
type = 'ext4' ;
what = mountOptions . diskPath ; // like /dev/disk/by-uuid/uuid or /dev/disk/by-id/scsi-id
options = 'discard,defaults,noatime' ;
break ;
2021-05-19 09:50:50 -07:00
case 'sshfs' : {
2021-05-18 16:49:39 +02:00
const keyFilePath = path . join ( paths . SSHFS _KEYS _DIR , ` id_rsa_ ${ mountOptions . host } ` ) ;
type = 'fuse.sshfs' ;
2021-06-24 16:59:47 -07:00
what = ` ${ mountOptions . user } @ ${ mountOptions . host } : ${ mountOptions . remoteDir } ` ;
2021-06-24 15:10:00 -07:00
options = ` allow_other,port= ${ mountOptions . port } ,IdentityFile= ${ keyFilePath } ,StrictHostKeyChecking=no,reconnect ` ; // allow_other means non-root users can access it
2021-05-18 16:49:39 +02:00
break ;
2021-05-14 15:07:29 -07:00
}
2021-06-24 23:01:18 -07:00
case 'filesystem' :
case 'mountpoint' :
2021-05-19 09:50:50 -07:00
return ;
}
2021-05-14 15:07:29 -07:00
2021-05-21 17:31:54 -07:00
return ejs . render ( SYSTEMD _MOUNT _EJS , { name , what , where : hostPath , options , type } ) ;
2021-05-14 15:07:29 -07:00
}
2021-09-28 11:43:15 -07:00
async function removeMount ( mount ) {
assert . strictEqual ( typeof mount , 'object' ) ;
2021-06-21 12:11:05 -07:00
2021-09-28 11:43:15 -07:00
const { hostPath , mountType , mountOptions } = mount ;
2021-05-14 15:07:29 -07:00
2021-05-17 16:23:24 -07:00
if ( constants . TEST ) return ;
2021-09-29 20:15:54 -07:00
await safe ( shell . promises . sudo ( 'removeMount' , [ RM _MOUNT _CMD , hostPath ] , { } ) , { debug } ) ; // ignore any error
2021-06-21 12:11:05 -07:00
if ( mountType === 'sshfs' ) {
const keyFilePath = path . join ( paths . SSHFS _KEYS _DIR , ` id_rsa_ ${ mountOptions . host } ` ) ;
safe . fs . unlinkSync ( keyFilePath ) ;
}
2021-05-14 15:07:29 -07:00
}
async function getStatus ( mountType , hostPath ) {
assert . strictEqual ( typeof mountType , 'string' ) ;
assert . strictEqual ( typeof hostPath , 'string' ) ;
2021-06-24 23:01:18 -07:00
if ( mountType === 'filesystem' ) return { state : 'active' , message : 'Mounted' } ;
2021-07-09 17:50:27 -07:00
const state = safe . child _process . execSync ( ` mountpoint -q -- ${ hostPath } ` ) ? 'active' : 'inactive' ;
if ( mountType === 'mountpoint' ) return { state , message : state === 'active' ? 'Mounted' : 'Not mounted' } ;
2021-05-14 15:07:29 -07:00
2021-07-09 17:50:27 -07:00
// we used to rely on "systemctl show -p ActiveState" output before but some mounts like sshfs.fuse show the status as "active" event though the mount commant failed (on ubuntu 18)
2021-05-14 15:07:29 -07:00
let message ;
if ( state !== 'active' ) { // find why it failed
2021-07-09 16:59:57 -07:00
const logsJson = safe . child _process . execSync ( ` journalctl -u $ (systemd-escape -p --suffix=mount ${ hostPath } ) -n 10 --no-pager -o json ` , { encoding : 'utf8' } ) ;
if ( logsJson ) {
const lines = logsJson . trim ( ) . split ( '\n' ) . map ( l => JSON . parse ( l ) ) ; // array of json
let start = - 1 , end = - 1 ; // start and end of error message block
for ( let idx = lines . length - 1 ; idx >= 0 ; idx -- ) { // reverse
const line = lines [ idx ] ;
const match = line [ 'SYSLOG_IDENTIFIER' ] === 'mount' || line [ '_EXE' ] . includes ( 'mount' ) || line [ '_COMM' ] . includes ( 'mount' ) ;
if ( match ) {
if ( end === - 1 ) end = idx ;
start = idx ;
continue ;
}
if ( end !== - 1 ) break ; // no match and we already found a block
}
if ( end !== - 1 ) message = lines . slice ( start , end + 1 ) . map ( line => line [ 'MESSAGE' ] ) . join ( '\n' ) ;
2021-05-14 15:07:29 -07:00
}
2021-05-17 15:58:38 -07:00
if ( ! message ) message = ` Could not determine failure reason. ${ safe . error ? safe . error . message : '' } ` ;
2021-05-14 15:07:29 -07:00
} else {
message = 'Mounted' ;
}
return { state , message } ;
}
2021-05-19 09:50:50 -07:00
2021-09-28 11:43:15 -07:00
async function tryAddMount ( mount , options ) {
assert . strictEqual ( typeof mount , 'object' ) ; // { name, hostPath, mountType, mountOptions }
2021-09-28 11:51:01 -07:00
assert . strictEqual ( typeof options , 'object' ) ; // { timeout, skipCleanup }
2021-05-19 09:50:50 -07:00
2021-09-28 11:43:15 -07:00
if ( mount . mountType === 'mountpoint' ) return ;
2021-05-21 17:31:54 -07:00
if ( constants . TEST ) return ;
2021-09-28 11:43:15 -07:00
if ( mount . mountType === 'sshfs' ) {
const keyFilePath = path . join ( paths . SSHFS _KEYS _DIR , ` id_rsa_ ${ mount . mountOptions . host } ` ) ;
2021-05-19 09:50:50 -07:00
2021-06-21 12:11:05 -07:00
safe . fs . mkdirSync ( paths . SSHFS _KEYS _DIR ) ;
2021-09-28 11:43:15 -07:00
if ( ! safe . fs . writeFileSync ( keyFilePath , ` ${ mount . mountOptions . privateKey } \n ` , { mode : 0o600 } ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2021-06-21 12:11:05 -07:00
}
2021-05-21 17:31:54 -07:00
2021-09-28 11:43:15 -07:00
const [ error ] = await safe ( shell . promises . sudo ( 'addMount' , [ ADD _MOUNT _CMD , renderMountFile ( mount ) , options . timeout ] , { } ) ) ;
2021-06-22 09:53:31 -07:00
if ( error && error . code === 2 ) throw new BoxError ( BoxError . MOUNT _ERROR , 'Failed to unmount existing mount' ) ; // at this point, the old mount config is still there
2021-05-21 17:31:54 -07:00
2021-09-28 11:51:01 -07:00
if ( options . skipCleanup ) return ;
2021-09-28 11:43:15 -07:00
const status = await getStatus ( mount . mountType , mount . hostPath ) ;
2021-06-21 12:11:05 -07:00
if ( status . state !== 'active' ) { // cleanup
2021-09-28 11:43:15 -07:00
await removeMount ( mount ) ;
2021-06-21 22:37:32 -07:00
throw new BoxError ( BoxError . MOUNT _ERROR , ` Failed to mount ( ${ status . state } ): ${ status . message } ` ) ;
2021-05-21 17:31:54 -07:00
}
2021-05-19 09:50:50 -07:00
}