2018-07-31 11:35:23 -07:00
'use strict' ;
exports = module . exports = {
updateToLatest : updateToLatest ,
2019-10-23 09:39:26 -07:00
update : update
2018-07-31 11:35:23 -07:00
} ;
2019-02-25 10:03:46 -08:00
var apps = require ( './apps.js' ) ,
assert = require ( 'assert' ) ,
2018-08-01 15:38:40 -07:00
async = require ( 'async' ) ,
2019-10-23 09:39:26 -07:00
BoxError = require ( './boxerror.js' ) ,
2018-08-01 15:38:40 -07:00
child _process = require ( 'child_process' ) ,
2018-07-31 11:35:23 -07:00
backups = require ( './backups.js' ) ,
2019-07-25 14:40:52 -07:00
constants = require ( './constants.js' ) ,
2018-08-01 15:38:40 -07:00
crypto = require ( 'crypto' ) ,
2018-07-31 11:35:23 -07:00
debug = require ( 'debug' ) ( 'box:updater' ) ,
2019-08-12 21:09:22 -07:00
df = require ( '@sindresorhus/df' ) ,
2018-12-09 03:20:00 -08:00
eventlog = require ( './eventlog.js' ) ,
locker = require ( './locker.js' ) ,
2018-08-01 15:38:40 -07:00
mkdirp = require ( 'mkdirp' ) ,
os = require ( 'os' ) ,
2018-07-31 11:35:23 -07:00
path = require ( 'path' ) ,
2018-08-01 15:38:40 -07:00
paths = require ( './paths.js' ) ,
safe = require ( 'safetydance' ) ,
2019-02-25 10:03:46 -08:00
semver = require ( 'semver' ) ,
2018-07-31 11:35:23 -07:00
shell = require ( './shell.js' ) ,
2018-11-19 20:01:02 -08:00
tasks = require ( './tasks.js' ) ,
2019-10-23 09:39:26 -07:00
updateChecker = require ( './updatechecker.js' ) ;
2018-07-31 11:35:23 -07:00
2018-08-01 15:38:40 -07:00
const RELEASES _PUBLIC _KEY = path . join ( _ _dirname , 'releases.gpg' ) ;
2018-07-31 11:35:23 -07:00
const UPDATE _CMD = path . join ( _ _dirname , 'scripts/update.sh' ) ;
2018-08-01 15:38:40 -07:00
function downloadUrl ( url , file , callback ) {
assert . strictEqual ( typeof file , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
// do not assert since it comes from the appstore
2019-10-23 09:39:26 -07:00
if ( typeof url !== 'string' ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , ` url cannot be download to ${ file } as it is not a string ` ) ) ;
2018-08-01 15:38:40 -07:00
let retryCount = 0 ;
safe . fs . unlinkSync ( file ) ;
async . retry ( { times : 10 , interval : 5000 } , function ( retryCallback ) {
debug ( ` Downloading ${ url } to ${ file } . Try ${ ++ retryCount } ` ) ;
const args = ` -s --fail ${ url } -o ${ file } ` ;
debug ( ` downloadUrl: curl ${ args } ` ) ;
2018-11-25 14:57:17 -08:00
shell . spawn ( 'downloadUrl' , '/usr/bin/curl' , args . split ( ' ' ) , { } , function ( error ) {
2019-10-23 09:39:26 -07:00
if ( error ) return retryCallback ( new BoxError ( BoxError . NETWORK _ERROR , ` Failed to download ${ url } : ${ error . message } ` ) ) ;
2018-08-01 15:38:40 -07:00
debug ( ` downloadUrl: downloadUrl ${ url } to ${ file } ` ) ;
retryCallback ( ) ;
} ) ;
} , callback ) ;
}
function gpgVerify ( file , sig , callback ) {
const cmd = ` /usr/bin/gpg --status-fd 1 --no-default-keyring --keyring ${ RELEASES _PUBLIC _KEY } --verify ${ sig } ${ file } ` ;
debug ( ` gpgVerify: ${ cmd } ` ) ;
child _process . exec ( cmd , { encoding : 'utf8' } , function ( error , stdout , stderr ) {
2019-10-23 09:39:26 -07:00
if ( error ) return callback ( new BoxError ( BoxError . NOT _SIGNED , ` The signature in ${ path . basename ( sig ) } could not verified ` ) ) ;
2018-08-01 15:38:40 -07:00
if ( stdout . indexOf ( '[GNUPG:] VALIDSIG 0EADB19CDDA23CD0FE71E3470A372F8703C493CC' ) ) return callback ( ) ;
debug ( ` gpgVerify: verification of ${ sig } failed: ${ stdout } \n ${ stderr } ` ) ;
2019-10-23 09:39:26 -07:00
return callback ( new BoxError ( BoxError . NOT _SIGNED , ` The signature in ${ path . basename ( sig ) } could not verified ` ) ) ;
2018-08-01 15:38:40 -07:00
} ) ;
}
function extractTarball ( tarball , dir , callback ) {
const args = ` -zxf ${ tarball } -C ${ dir } ` ;
debug ( ` extractTarball: tar ${ args } ` ) ;
2018-11-25 14:57:17 -08:00
shell . spawn ( 'extractTarball' , '/bin/tar' , args . split ( ' ' ) , { } , function ( error ) {
2019-10-23 09:39:26 -07:00
if ( error ) return callback ( new BoxError ( BoxError . FS _ERROR , ` Failed to extract release package: ${ error . message } ` ) ) ;
2018-08-01 15:38:40 -07:00
safe . fs . unlinkSync ( tarball ) ;
debug ( ` extractTarball: extracted ${ tarball } to ${ dir } ` ) ;
callback ( ) ;
} ) ;
}
function verifyUpdateInfo ( versionsFile , updateInfo , callback ) {
assert . strictEqual ( typeof versionsFile , 'string' ) ;
assert . strictEqual ( typeof updateInfo , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var releases = safe . JSON . parse ( safe . fs . readFileSync ( versionsFile , 'utf8' ) ) || { } ;
2019-10-23 09:39:26 -07:00
if ( ! releases [ constants . VERSION ] || ! releases [ constants . VERSION ] . next ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'No version info' ) ) ;
2019-07-25 14:40:52 -07:00
var nextVersion = releases [ constants . VERSION ] . next ;
2019-10-23 09:39:26 -07:00
if ( typeof releases [ nextVersion ] !== 'object' || ! releases [ nextVersion ] ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'No next version info' ) ) ;
if ( releases [ nextVersion ] . sourceTarballUrl !== updateInfo . sourceTarballUrl ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Version info mismatch' ) ) ;
2018-08-01 15:38:40 -07:00
callback ( ) ;
}
function downloadAndVerifyRelease ( updateInfo , callback ) {
assert . strictEqual ( typeof updateInfo , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
let newBoxSource = path . join ( os . tmpdir ( ) , 'box-' + crypto . randomBytes ( 4 ) . readUInt32LE ( 0 ) ) ;
async . series ( [
downloadUrl . bind ( null , updateInfo . boxVersionsUrl , ` ${ paths . UPDATE _DIR } /versions.json ` ) ,
downloadUrl . bind ( null , updateInfo . boxVersionsSigUrl , ` ${ paths . UPDATE _DIR } /versions.json.sig ` ) ,
gpgVerify . bind ( null , ` ${ paths . UPDATE _DIR } /versions.json ` , ` ${ paths . UPDATE _DIR } /versions.json.sig ` ) ,
verifyUpdateInfo . bind ( null , ` ${ paths . UPDATE _DIR } /versions.json ` , updateInfo ) ,
downloadUrl . bind ( null , updateInfo . sourceTarballUrl , ` ${ paths . UPDATE _DIR } /box.tar.gz ` ) ,
downloadUrl . bind ( null , updateInfo . sourceTarballSigUrl , ` ${ paths . UPDATE _DIR } /box.tar.gz.sig ` ) ,
gpgVerify . bind ( null , ` ${ paths . UPDATE _DIR } /box.tar.gz ` , ` ${ paths . UPDATE _DIR } /box.tar.gz.sig ` ) ,
mkdirp . bind ( null , newBoxSource ) ,
extractTarball . bind ( null , ` ${ paths . UPDATE _DIR } /box.tar.gz ` , newBoxSource )
] , function ( error ) {
if ( error ) return callback ( error ) ;
callback ( null , { file : newBoxSource } ) ;
} ) ;
}
2019-08-12 21:09:22 -07:00
function checkFreeDiskSpace ( neededSpace , callback ) {
assert . strictEqual ( typeof neededSpace , 'number' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
// can probably be a bit more aggressive here since a new update can bring in new docker images
df . file ( '/' ) . then ( function ( diskUsage ) {
2019-10-23 09:39:26 -07:00
if ( diskUsage . available < neededSpace ) return callback ( new BoxError ( BoxError . FS _ERROR , 'Not enough disk space' ) ) ;
2019-08-12 21:47:22 -07:00
callback ( null ) ;
2019-08-12 21:09:22 -07:00
} ) . catch ( function ( error ) {
2019-10-23 09:39:26 -07:00
callback ( new BoxError ( BoxError . FS _ERROR , error ) ) ;
2019-08-12 21:09:22 -07:00
} ) ;
}
2019-05-12 13:28:53 -07:00
function update ( boxUpdateInfo , options , progressCallback , callback ) {
2018-08-01 15:38:40 -07:00
assert ( boxUpdateInfo && typeof boxUpdateInfo === 'object' ) ;
2019-05-12 13:28:53 -07:00
assert ( options && typeof options === 'object' ) ;
2018-11-30 16:00:47 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-08-01 15:38:40 -07:00
2019-08-12 21:09:22 -07:00
progressCallback ( { percent : 1 , message : 'Checking disk space' } ) ;
2018-08-01 15:38:40 -07:00
2019-08-12 21:09:22 -07:00
checkFreeDiskSpace ( 1024 * 1024 * 1024 , function ( error ) {
2018-11-30 16:00:47 -08:00
if ( error ) return callback ( error ) ;
2018-08-01 15:38:40 -07:00
2019-08-12 21:09:22 -07:00
progressCallback ( { percent : 5 , message : 'Downloading and verifying release' } ) ;
2019-05-12 13:28:53 -07:00
2019-08-12 21:09:22 -07:00
downloadAndVerifyRelease ( boxUpdateInfo , function ( error , packageInfo ) {
2018-11-30 16:00:47 -08:00
if ( error ) return callback ( error ) ;
2018-08-01 15:38:40 -07:00
2019-08-12 21:09:22 -07:00
function maybeBackup ( next ) {
if ( options . skipBackup ) return next ( ) ;
progressCallback ( { percent : 10 , message : 'Backing up' } ) ;
2018-08-01 15:38:40 -07:00
2019-08-12 21:09:22 -07:00
backups . backupBoxAndApps ( ( progress ) => progressCallback ( { percent : 10 + progress . percent * 70 / 100 , message : progress . message } ) , next ) ;
}
2018-08-01 15:38:40 -07:00
2019-08-12 21:09:22 -07:00
maybeBackup ( function ( error ) {
2018-11-30 16:00:47 -08:00
if ( error ) return callback ( error ) ;
2018-08-01 15:38:40 -07:00
2019-08-12 21:09:22 -07:00
debug ( 'updating box %s' , boxUpdateInfo . sourceTarballUrl ) ;
progressCallback ( { percent : 70 , message : 'Installing update' } ) ;
// run installer.sh from new box code as a separate service
shell . sudo ( 'update' , [ UPDATE _CMD , packageInfo . file ] , { } , function ( error ) {
if ( error ) return callback ( error ) ;
// Do not add any code here. The installer script will stop the box code any instant
} ) ;
2018-08-01 15:38:40 -07:00
} ) ;
} ) ;
} ) ;
}
2018-07-31 11:35:23 -07:00
2019-02-25 10:03:46 -08:00
function canUpdate ( boxUpdateInfo , callback ) {
assert . strictEqual ( typeof boxUpdateInfo , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
apps . getAll ( function ( error , result ) {
2019-10-24 20:31:45 -07:00
if ( error ) return callback ( error ) ;
2019-02-25 10:03:46 -08:00
for ( let app of result ) {
const maxBoxVersion = app . manifest . maxBoxVersion ;
if ( semver . valid ( maxBoxVersion ) && semver . gt ( boxUpdateInfo . version , maxBoxVersion ) ) {
2019-10-23 09:39:26 -07:00
return callback ( new BoxError ( BoxError . BAD _STATE , ` Cannot update to v ${ boxUpdateInfo . version } because ${ app . fqdn } has a maxBoxVersion of ${ maxBoxVersion } ` ) ) ;
2019-02-25 10:03:46 -08:00
}
}
callback ( ) ;
} ) ;
}
2019-05-12 13:28:53 -07:00
function updateToLatest ( options , auditSource , callback ) {
assert . strictEqual ( typeof options , 'object' ) ;
2018-07-31 11:35:23 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var boxUpdateInfo = updateChecker . getUpdateInfo ( ) . box ;
2019-10-23 09:39:26 -07:00
if ( ! boxUpdateInfo ) return callback ( new BoxError ( BoxError . NOT _FOUND , 'No update available' ) ) ;
if ( ! boxUpdateInfo . sourceTarballUrl ) return callback ( new BoxError ( BoxError . BAD _STATE , 'No automatic update available' ) ) ;
2018-07-31 11:35:23 -07:00
2019-02-25 10:03:46 -08:00
canUpdate ( boxUpdateInfo , function ( error ) {
if ( error ) return callback ( error ) ;
2018-12-04 14:04:43 -08:00
2019-02-25 10:03:46 -08:00
error = locker . lock ( locker . OP _BOX _UPDATE ) ;
2019-10-23 09:39:26 -07:00
if ( error ) return callback ( new BoxError ( BoxError . BAD _STATE , ` Cannot update now: ${ error . message } ` ) ) ;
2018-12-09 03:20:00 -08:00
2019-08-27 22:39:59 -07:00
tasks . add ( tasks . TASK _UPDATE , [ boxUpdateInfo , options ] , function ( error , taskId ) {
2019-10-23 09:39:26 -07:00
if ( error ) return callback ( error ) ;
2019-08-27 22:39:59 -07:00
2019-02-25 10:03:46 -08:00
eventlog . add ( eventlog . ACTION _UPDATE , auditSource , { taskId , boxUpdateInfo } ) ;
2019-10-14 09:30:20 -07:00
tasks . startTask ( taskId , { timeout : 20 * 60 * 60 * 1000 /* 20 hours */ } , ( error ) => {
2019-08-27 22:39:59 -07:00
locker . unlock ( locker . OP _BOX _UPDATE ) ;
debug ( 'Update failed with error' , error ) ;
2019-10-14 09:30:20 -07:00
const timedOut = error . code === tasks . ETIMEOUT ;
eventlog . add ( eventlog . ACTION _UPDATE _FINISH , auditSource , { taskId , errorMessage : error . message , timedOut } ) ;
2019-08-27 22:39:59 -07:00
} ) ;
callback ( null , taskId ) ;
2019-02-25 10:03:46 -08:00
} ) ;
2018-12-09 03:20:00 -08:00
} ) ;
2018-07-31 11:35:23 -07:00
}