2015-07-20 00:09:47 -07:00
/* jslint node: true */
'use strict' ;
exports = module . exports = {
CloudronError : CloudronError ,
initialize : initialize ,
uninitialize : uninitialize ,
activate : activate ,
getConfig : getConfig ,
getStatus : getStatus ,
sendHeartbeat : sendHeartbeat ,
update : update ,
reboot : reboot ,
migrate : migrate ,
backup : backup ,
2015-10-27 16:00:31 -07:00
ensureBackup : ensureBackup ,
2015-11-03 15:50:02 -08:00
isConfiguredSync : isConfiguredSync ,
2015-10-27 16:11:33 -07:00
2015-10-27 16:00:31 -07:00
events : new ( require ( 'events' ) . EventEmitter ) ( ) ,
2015-11-03 15:50:02 -08:00
EVENT _ACTIVATED : 'activated' ,
EVENT _CONFIGURED : 'configured'
2015-09-28 23:10:09 -07:00
} ;
2015-07-20 00:09:47 -07:00
var apps = require ( './apps.js' ) ,
AppsError = require ( './apps.js' ) . AppsError ,
assert = require ( 'assert' ) ,
async = require ( 'async' ) ,
backups = require ( './backups.js' ) ,
BackupsError = require ( './backups.js' ) . BackupsError ,
clientdb = require ( './clientdb.js' ) ,
config = require ( './config.js' ) ,
debug = require ( 'debug' ) ( 'box:cloudron' ) ,
fs = require ( 'fs' ) ,
locker = require ( './locker.js' ) ,
2015-12-31 10:30:42 +01:00
os = require ( 'os' ) ,
2015-07-20 00:09:47 -07:00
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
progress = require ( './progress.js' ) ,
safe = require ( 'safetydance' ) ,
settings = require ( './settings.js' ) ,
shell = require ( './shell.js' ) ,
2015-08-26 16:14:51 -07:00
subdomains = require ( './subdomains.js' ) ,
2015-07-20 00:09:47 -07:00
superagent = require ( 'superagent' ) ,
sysinfo = require ( './sysinfo.js' ) ,
tokendb = require ( './tokendb.js' ) ,
updateChecker = require ( './updatechecker.js' ) ,
user = require ( './user.js' ) ,
UserError = user . UserError ,
userdb = require ( './userdb.js' ) ,
2015-08-24 12:19:31 -07:00
util = require ( 'util' ) ,
webhooks = require ( './webhooks.js' ) ;
2015-07-20 00:09:47 -07:00
2015-10-27 18:38:13 +01:00
var REBOOT _CMD = path . join ( _ _dirname , 'scripts/reboot.sh' ) ,
2015-07-20 00:09:47 -07:00
BACKUP _BOX _CMD = path . join ( _ _dirname , 'scripts/backupbox.sh' ) ,
BACKUP _SWAP _CMD = path . join ( _ _dirname , 'scripts/backupswap.sh' ) ,
INSTALLER _UPDATE _URL = 'http://127.0.0.1:2020/api/v1/installer/update' ;
2015-10-29 12:28:50 -07:00
var NOOP _CALLBACK = function ( error ) { if ( error ) debug ( error ) ; } ;
2015-10-27 21:10:00 -07:00
2015-10-29 10:54:28 -07:00
var gUpdatingDns = false , // flag for dns update reentrancy
2015-10-27 16:11:33 -07:00
gCloudronDetails = null , // cached cloudron details like region,size...
2015-11-03 15:50:02 -08:00
gIsConfigured = null ; // cached configured state so that return value is synchronous. null means we are not initialized yet
2015-07-20 00:09:47 -07:00
function debugApp ( app , args ) {
assert ( ! app || typeof app === 'object' ) ;
var prefix = app ? app . location : '(no app)' ;
debug ( prefix + ' ' + util . format . apply ( util , Array . prototype . slice . call ( arguments , 1 ) ) ) ;
}
function ignoreError ( func ) {
return function ( callback ) {
func ( function ( error ) {
if ( error ) console . error ( 'Ignored error:' , error ) ;
callback ( ) ;
} ) ;
} ;
}
function CloudronError ( reason , errorOrMessage ) {
assert . strictEqual ( typeof reason , 'string' ) ;
assert ( errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' ) ;
Error . call ( this ) ;
Error . captureStackTrace ( this , this . constructor ) ;
this . name = this . constructor . name ;
this . reason = reason ;
if ( typeof errorOrMessage === 'undefined' ) {
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
util . inherits ( CloudronError , Error ) ;
CloudronError . BAD _FIELD = 'Field error' ;
CloudronError . INTERNAL _ERROR = 'Internal Error' ;
CloudronError . EXTERNAL _ERROR = 'External Error' ;
CloudronError . ALREADY _PROVISIONED = 'Already Provisioned' ;
CloudronError . BAD _USERNAME = 'Bad username' ;
CloudronError . BAD _EMAIL = 'Bad email' ;
CloudronError . BAD _PASSWORD = 'Bad password' ;
CloudronError . BAD _NAME = 'Bad name' ;
CloudronError . BAD _STATE = 'Bad state' ;
CloudronError . NOT _FOUND = 'Not found' ;
function initialize ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2015-11-03 15:50:02 -08:00
exports . events . on ( exports . EVENT _CONFIGURED , addDnsRecords ) ;
2015-07-20 00:09:47 -07:00
2015-11-03 15:50:02 -08:00
syncConfigState ( callback ) ;
2015-07-20 00:09:47 -07:00
}
function uninitialize ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2015-11-03 15:50:02 -08:00
exports . events . removeListener ( exports . EVENT _CONFIGURED , addDnsRecords ) ;
2015-11-03 15:22:02 -08:00
2015-07-20 00:09:47 -07:00
callback ( null ) ;
}
2015-11-03 15:50:02 -08:00
function isConfiguredSync ( ) {
return gIsConfigured === true ;
}
function isConfigured ( callback ) {
// set of rules to see if we have the configs required for cloudron to function
// note this checks for missing configs and not invalid configs
settings . getDnsConfig ( function ( error , dnsConfig ) {
if ( error ) return callback ( error ) ;
if ( ! dnsConfig ) return callback ( null , false ) ;
var isConfigured = ( config . isCustomDomain ( ) && dnsConfig . provider === 'route53' ) ||
( ! config . isCustomDomain ( ) && dnsConfig . provider === 'caas' ) ;
2015-11-04 08:28:21 -08:00
callback ( null , isConfigured ) ;
2015-11-03 15:50:02 -08:00
} ) ;
}
function syncConfigState ( callback ) {
assert ( ! gIsConfigured ) ;
callback = callback || NOOP _CALLBACK ;
isConfigured ( function ( error , configured ) {
if ( error ) return callback ( error ) ;
2015-11-03 16:11:24 -08:00
debug ( 'syncConfigState: configured = %s' , configured ) ;
2015-11-03 15:50:02 -08:00
if ( configured ) {
exports . events . emit ( exports . EVENT _CONFIGURED ) ;
} else {
settings . events . once ( settings . DNS _CONFIG _KEY , function ( ) { syncConfigState ( ) ; } ) ; // check again later
}
gIsConfigured = configured ;
callback ( ) ;
} ) ;
2015-10-27 16:11:33 -07:00
}
2015-07-20 00:09:47 -07:00
function setTimeZone ( ip , callback ) {
assert . strictEqual ( typeof ip , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'setTimeZone ip:%s' , ip ) ;
superagent . get ( 'http://www.telize.com/geoip/' + ip ) . end ( function ( error , result ) {
2015-12-15 09:12:52 -08:00
if ( ( error && ! error . response ) || result . statusCode !== 200 ) {
2015-12-17 19:35:52 -08:00
debug ( 'Failed to get geo location: %s' , error . message ) ;
2015-07-20 00:09:47 -07:00
return callback ( null ) ;
}
if ( ! result . body . timezone ) {
2015-09-18 12:03:48 -07:00
debug ( 'No timezone in geoip response : %j' , result . body ) ;
2015-07-20 00:09:47 -07:00
return callback ( null ) ;
}
debug ( 'Setting timezone to ' , result . body . timezone ) ;
settings . setTimeZone ( result . body . timezone , callback ) ;
} ) ;
}
2015-10-20 13:12:37 +02:00
function activate ( username , password , email , ip , callback ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof email , 'string' ) ;
assert . strictEqual ( typeof ip , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'activating user:%s email:%s' , username , email ) ;
2015-09-18 13:40:22 -07:00
setTimeZone ( ip , function ( ) { } ) ; // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
2015-07-20 00:09:47 -07:00
2015-10-20 13:12:37 +02:00
user . createOwner ( username , password , email , function ( error , userObject ) {
if ( error && error . reason === UserError . ALREADY _EXISTS ) return callback ( new CloudronError ( CloudronError . ALREADY _PROVISIONED ) ) ;
if ( error && error . reason === UserError . BAD _USERNAME ) return callback ( new CloudronError ( CloudronError . BAD _USERNAME ) ) ;
if ( error && error . reason === UserError . BAD _PASSWORD ) return callback ( new CloudronError ( CloudronError . BAD _PASSWORD ) ) ;
if ( error && error . reason === UserError . BAD _EMAIL ) return callback ( new CloudronError ( CloudronError . BAD _EMAIL ) ) ;
2015-07-20 00:09:47 -07:00
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2015-10-20 13:12:37 +02:00
clientdb . getByAppIdAndType ( 'webadmin' , clientdb . TYPE _ADMIN , function ( error , result ) {
2015-07-20 00:09:47 -07:00
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2015-10-20 13:12:37 +02:00
// Also generate a token so the admin creation can also act as a login
var token = tokendb . generateToken ( ) ;
var expires = Date . now ( ) + 24 * 60 * 60 * 1000 ; // 1 day
2015-07-20 00:09:47 -07:00
2015-10-20 13:12:37 +02:00
tokendb . add ( token , tokendb . PREFIX _USER + userObject . id , result . id , expires , '*' , function ( error ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2015-10-27 22:18:02 -07:00
// EE API is sync. do not keep the REST API reponse waiting
process . nextTick ( function ( ) { exports . events . emit ( exports . EVENT _ACTIVATED ) ; } ) ;
2015-10-27 16:00:31 -07:00
2015-10-20 13:12:37 +02:00
callback ( null , { token : token , expires : expires } ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
} ) ;
}
function getStatus ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
userdb . count ( function ( error , count ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
settings . getCloudronName ( function ( error , cloudronName ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
callback ( null , {
activated : count !== 0 ,
version : config . version ( ) ,
2015-11-09 17:50:09 -08:00
boxVersionsUrl : config . get ( 'boxVersionsUrl' ) ,
apiServerOrigin : config . apiServerOrigin ( ) , // used by CaaS tool
2015-07-20 00:09:47 -07:00
cloudronName : cloudronName
} ) ;
} ) ;
} ) ;
}
function getCloudronDetails ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
if ( gCloudronDetails ) return callback ( null , gCloudronDetails ) ;
2015-12-29 17:43:54 +01:00
if ( ! config . token ( ) ) {
gCloudronDetails = {
region : null ,
size : null
} ;
return callback ( null , gCloudronDetails ) ;
}
2015-07-20 00:09:47 -07:00
superagent
. get ( config . apiServerOrigin ( ) + '/api/v1/boxes/' + config . fqdn ( ) )
. query ( { token : config . token ( ) } )
. end ( function ( error , result ) {
2015-12-15 09:12:52 -08:00
if ( error && ! error . response ) return callback ( error ) ;
if ( result . statusCode !== 200 ) return callback ( new CloudronError ( CloudronError . EXTERNAL _ERROR , util . format ( '%s %j' , result . status , result . body ) ) ) ;
2015-07-20 00:09:47 -07:00
gCloudronDetails = result . body . box ;
return callback ( null , gCloudronDetails ) ;
} ) ;
}
function getConfig ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
getCloudronDetails ( function ( error , result ) {
if ( error ) {
2015-11-09 19:10:33 -08:00
debug ( 'Failed to fetch cloudron details.' , error ) ;
2015-07-20 00:09:47 -07:00
// set fallback values to avoid dependency on appstore
result = {
region : result ? result . region : null ,
size : result ? result . size : null
} ;
}
settings . getCloudronName ( function ( error , cloudronName ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2015-07-23 12:52:04 +02:00
settings . getDeveloperMode ( function ( error , developerMode ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
callback ( null , {
apiServerOrigin : config . apiServerOrigin ( ) ,
webServerOrigin : config . webServerOrigin ( ) ,
isDev : config . isDev ( ) ,
fqdn : config . fqdn ( ) ,
ip : sysinfo . getIp ( ) ,
version : config . version ( ) ,
update : updateChecker . getUpdateInfo ( ) ,
progress : progress . get ( ) ,
isCustomDomain : config . isCustomDomain ( ) ,
developerMode : developerMode ,
region : result . region ,
size : result . size ,
2015-12-31 10:30:42 +01:00
memory : os . totalmem ( ) ,
2015-12-29 11:24:34 +01:00
provider : config . provider ( ) ,
2015-07-23 12:52:04 +02:00
cloudronName : cloudronName
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
} ) ;
}
function sendHeartbeat ( ) {
2015-12-29 17:43:54 +01:00
if ( ! config . token ( ) ) return ;
2015-07-20 00:09:47 -07:00
2015-12-29 17:43:54 +01:00
var url = config . apiServerOrigin ( ) + '/api/v1/boxes/' + config . fqdn ( ) + '/heartbeat' ;
2015-08-22 16:51:56 -07:00
superagent . post ( url ) . query ( { token : config . token ( ) , version : config . version ( ) } ) . timeout ( 10000 ) . end ( function ( error , result ) {
2015-12-15 09:12:52 -08:00
if ( error && ! error . response ) debug ( 'Network error sending heartbeat.' , error ) ;
2015-07-20 00:09:47 -07:00
else if ( result . statusCode !== 200 ) debug ( 'Server responded to heartbeat with %s %s' , result . statusCode , result . text ) ;
2015-07-28 09:32:14 -07:00
else debug ( 'Heartbeat sent to %s' , url ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2015-10-29 10:54:28 -07:00
function readDkimPublicKeySync ( ) {
2015-07-20 00:09:47 -07:00
var dkimPublicKeyFile = path . join ( paths . MAIL _DATA _DIR , 'dkim/' + config . fqdn ( ) + '/public' ) ;
var publicKey = safe . fs . readFileSync ( dkimPublicKeyFile , 'utf8' ) ;
2015-08-30 21:44:59 -07:00
if ( publicKey === null ) {
2015-10-29 10:54:28 -07:00
debug ( 'Error reading dkim public key.' , safe . error ) ;
return null ;
2015-08-30 21:44:59 -07:00
}
2015-07-20 00:09:47 -07:00
// remove header, footer and new lines
publicKey = publicKey . split ( '\n' ) . slice ( 1 , - 2 ) . join ( '' ) ;
2015-10-29 10:54:28 -07:00
return publicKey ;
}
2015-07-20 00:09:47 -07:00
2015-10-30 13:53:12 -07:00
function txtRecordsWithSpf ( callback ) {
2015-10-29 10:54:28 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2015-07-20 00:09:47 -07:00
2015-10-29 15:35:07 -07:00
subdomains . get ( '' , 'TXT' , function ( error , txtRecords ) {
2015-10-29 10:54:28 -07:00
if ( error ) return callback ( error ) ;
2015-10-30 18:12:24 -07:00
debug ( 'txtRecordsWithSpf: current txt records - %j' , txtRecords ) ;
2015-10-30 13:53:12 -07:00
var i , validSpf ;
2015-10-29 10:54:28 -07:00
2015-10-29 15:35:07 -07:00
for ( i = 0 ; i < txtRecords . length ; i ++ ) {
2015-10-30 16:04:09 -07:00
if ( txtRecords [ i ] . indexOf ( '"v=spf1 ' ) !== 0 ) continue ; // not SPF
2015-10-29 10:54:28 -07:00
2015-10-30 18:12:24 -07:00
validSpf = txtRecords [ i ] . indexOf ( ' a:' + config . fqdn ( ) + ' ' ) !== - 1 ;
2015-10-29 10:54:28 -07:00
break ;
2015-07-20 00:09:47 -07:00
}
2015-10-29 10:54:28 -07:00
if ( validSpf ) return callback ( null , null ) ;
2015-10-29 15:35:07 -07:00
if ( i == txtRecords . length ) {
2015-10-30 13:45:07 -07:00
txtRecords [ i ] = '"v=spf1 a:' + config . fqdn ( ) + ' ~all"' ;
2015-10-29 10:54:28 -07:00
} else {
2015-11-04 14:22:56 -08:00
txtRecords [ i ] = '"v=spf1 a:' + config . fqdn ( ) + ' ' + txtRecords [ i ] . slice ( '"v=spf1 ' . length ) ;
2015-10-29 10:54:28 -07:00
}
2015-10-30 13:45:07 -07:00
return callback ( null , txtRecords ) ;
2015-10-29 10:54:28 -07:00
} ) ;
}
2015-11-03 15:22:02 -08:00
function addDnsRecords ( ) {
var callback = NOOP _CALLBACK ;
2015-10-29 10:54:28 -07:00
2015-10-30 12:50:27 -07:00
if ( process . env . BOX _ENV === 'test' ) return callback ( ) ;
2015-10-29 15:34:30 -07:00
if ( gUpdatingDns ) {
debug ( 'addDnsRecords: dns update already in progress' ) ;
return callback ( ) ;
}
2015-10-29 10:54:28 -07:00
gUpdatingDns = true ;
var DKIM _SELECTOR = 'cloudron' ;
var DMARC _REPORT _EMAIL = 'dmarc-report@cloudron.io' ;
var dkimKey = readDkimPublicKeySync ( ) ;
if ( ! dkimKey ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , new Error ( 'internal error failed to read dkim public key' ) ) ) ;
2015-10-30 13:45:07 -07:00
var nakedDomainRecord = { subdomain : '' , type : 'A' , values : [ sysinfo . getIp ( ) ] } ;
var webadminRecord = { subdomain : 'my' , type : 'A' , values : [ sysinfo . getIp ( ) ] } ;
2015-10-29 10:54:28 -07:00
// t=s limits the domainkey to this domain and not it's subdomains
2015-10-30 13:45:07 -07:00
var dkimRecord = { subdomain : DKIM _SELECTOR + '._domainkey' , type : 'TXT' , values : [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] } ;
2015-10-29 10:54:28 -07:00
// DMARC requires special setup if report email id is in different domain
2015-10-30 13:45:07 -07:00
var dmarcRecord = { subdomain : '_dmarc' , type : 'TXT' , values : [ '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC _REPORT _EMAIL + '; ruf=' + DMARC _REPORT _EMAIL + '"' ] } ;
2015-10-29 10:54:28 -07:00
var records = [ ] ;
if ( config . isCustomDomain ( ) ) {
records . push ( webadminRecord ) ;
2015-11-05 09:27:27 -08:00
records . push ( dkimRecord ) ;
2015-10-29 10:54:28 -07:00
} else {
records . push ( nakedDomainRecord ) ;
records . push ( webadminRecord ) ;
records . push ( dkimRecord ) ;
records . push ( dmarcRecord ) ;
}
2015-10-29 15:34:30 -07:00
debug ( 'addDnsRecords: %j' , records ) ;
2015-10-29 10:54:28 -07:00
async . retry ( { times : 10 , interval : 20000 } , function ( retryCallback ) {
2015-10-30 13:53:12 -07:00
txtRecordsWithSpf ( function ( error , txtRecords ) {
2015-10-30 13:48:46 -07:00
if ( error ) return retryCallback ( error ) ;
2015-10-29 10:54:28 -07:00
2015-10-30 13:53:12 -07:00
if ( txtRecords ) records . push ( { subdomain : '' , type : 'TXT' , values : txtRecords } ) ;
2015-10-29 10:54:28 -07:00
2015-10-30 13:48:46 -07:00
debug ( 'addDnsRecords: will update %j' , records ) ;
2015-11-19 17:49:20 -08:00
async . mapSeries ( records , function ( record , iteratorCallback ) {
2015-10-30 13:48:46 -07:00
subdomains . update ( record . subdomain , record . type , record . values , iteratorCallback ) ;
2015-11-19 17:49:20 -08:00
} , function ( error , changeIds ) {
2015-11-04 14:28:22 -08:00
if ( error ) debug ( 'addDnsRecords: failed to update : %s. will retry' , error ) ;
2015-11-19 17:49:20 -08:00
else debug ( 'addDnsRecords: records %j added with changeIds %j' , records , changeIds ) ;
2015-11-04 14:28:22 -08:00
retryCallback ( error ) ;
} ) ;
2015-10-30 13:48:46 -07:00
} ) ;
2015-10-29 10:54:28 -07:00
} , function ( error ) {
gUpdatingDns = false ;
2015-10-30 18:12:24 -07:00
debug ( 'addDnsRecords: done updating records with error:' , error ) ;
2015-10-29 10:54:28 -07:00
callback ( error ) ;
2015-08-30 21:29:02 -07:00
} ) ;
}
2015-07-20 00:09:47 -07:00
function reboot ( callback ) {
shell . sudo ( 'reboot' , [ REBOOT _CMD ] , callback ) ;
}
function migrate ( size , region , callback ) {
assert . strictEqual ( typeof size , 'string' ) ;
assert . strictEqual ( typeof region , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var error = locker . lock ( locker . OP _MIGRATE ) ;
if ( error ) return callback ( new CloudronError ( CloudronError . BAD _STATE , error . message ) ) ;
function unlock ( error ) {
if ( error ) {
debug ( 'Failed to migrate' , error ) ;
locker . unlock ( locker . OP _MIGRATE ) ;
} else {
debug ( 'Migration initiated successfully' ) ;
// do not unlock; cloudron is migrating
}
return ;
}
// initiate the migration in the background
backupBoxAndApps ( function ( error , restoreKey ) {
if ( error ) return unlock ( error ) ;
debug ( 'migrate: size %s region %s restoreKey %s' , size , region , restoreKey ) ;
superagent
. post ( config . apiServerOrigin ( ) + '/api/v1/boxes/' + config . fqdn ( ) + '/migrate' )
. query ( { token : config . token ( ) } )
. send ( { size : size , region : region , restoreKey : restoreKey } )
. end ( function ( error , result ) {
2015-12-15 09:12:52 -08:00
if ( error && ! error . response ) return unlock ( error ) ;
if ( result . statusCode === 409 ) return unlock ( new CloudronError ( CloudronError . BAD _STATE ) ) ;
if ( result . statusCode === 404 ) return unlock ( new CloudronError ( CloudronError . NOT _FOUND ) ) ;
if ( result . statusCode !== 202 ) return unlock ( new CloudronError ( CloudronError . EXTERNAL _ERROR , util . format ( '%s %j' , result . status , result . body ) ) ) ;
2015-07-20 00:09:47 -07:00
return unlock ( null ) ;
} ) ;
} ) ;
callback ( null ) ;
}
function update ( boxUpdateInfo , callback ) {
assert . strictEqual ( typeof boxUpdateInfo , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
if ( ! boxUpdateInfo ) return callback ( null ) ;
var error = locker . lock ( locker . OP _BOX _UPDATE ) ;
if ( error ) return callback ( new CloudronError ( CloudronError . BAD _STATE , error . message ) ) ;
// initiate the update/upgrade but do not wait for it
if ( boxUpdateInfo . upgrade ) {
2015-07-28 14:22:08 -07:00
debug ( 'Starting upgrade' ) ;
2015-07-20 00:09:47 -07:00
doUpgrade ( boxUpdateInfo , function ( error ) {
2015-07-28 15:28:10 -07:00
if ( error ) {
2015-11-26 12:04:39 +01:00
console . error ( 'Upgrade failed with error:' , error ) ;
2015-07-28 15:28:10 -07:00
locker . unlock ( locker . OP _BOX _UPDATE ) ;
}
2015-07-20 00:09:47 -07:00
} ) ;
} else {
2015-07-28 14:22:08 -07:00
debug ( 'Starting update' ) ;
2015-07-20 00:09:47 -07:00
doUpdate ( boxUpdateInfo , function ( error ) {
2015-07-28 15:28:10 -07:00
if ( error ) {
2015-11-26 12:04:39 +01:00
console . error ( 'Update failed with error:' , error ) ;
2015-07-28 15:28:10 -07:00
locker . unlock ( locker . OP _BOX _UPDATE ) ;
}
2015-07-20 00:09:47 -07:00
} ) ;
}
callback ( null ) ;
}
function doUpgrade ( boxUpdateInfo , callback ) {
assert ( boxUpdateInfo !== null && typeof boxUpdateInfo === 'object' ) ;
2015-07-28 14:40:22 -07:00
function upgradeError ( e ) {
2015-07-29 13:53:16 +02:00
progress . set ( progress . UPDATE , - 1 , e . message ) ;
2015-07-28 14:40:22 -07:00
callback ( e ) ;
}
2015-09-22 13:01:56 -07:00
progress . set ( progress . UPDATE , 5 , 'Backing up for upgrade' ) ;
2015-07-20 00:09:47 -07:00
backupBoxAndApps ( function ( error ) {
2015-07-29 12:33:07 +02:00
if ( error ) return upgradeError ( error ) ;
2015-07-20 00:09:47 -07:00
superagent . post ( config . apiServerOrigin ( ) + '/api/v1/boxes/' + config . fqdn ( ) + '/upgrade' )
. query ( { token : config . token ( ) } )
. send ( { version : boxUpdateInfo . version } )
. end ( function ( error , result ) {
2015-12-15 09:12:52 -08:00
if ( error && ! error . response ) return upgradeError ( new Error ( 'Network error making upgrade request: ' + error ) ) ;
if ( result . statusCode !== 202 ) return upgradeError ( new Error ( util . format ( 'Server not ready to upgrade. statusCode: %s body: %j' , result . status , result . body ) ) ) ;
2015-07-20 00:09:47 -07:00
progress . set ( progress . UPDATE , 10 , 'Updating base system' ) ;
// no need to unlock since this is the last thing we ever do on this box
callback ( null ) ;
} ) ;
} ) ;
}
function doUpdate ( boxUpdateInfo , callback ) {
assert ( boxUpdateInfo && typeof boxUpdateInfo === 'object' ) ;
2015-07-29 13:52:59 +02:00
function updateError ( e ) {
2015-07-29 13:53:16 +02:00
progress . set ( progress . UPDATE , - 1 , e . message ) ;
2015-07-28 14:40:22 -07:00
callback ( e ) ;
}
2015-09-22 13:01:56 -07:00
progress . set ( progress . UPDATE , 5 , 'Backing up for update' ) ;
2015-07-20 00:09:47 -07:00
2015-09-22 12:51:58 -07:00
backupBoxAndApps ( function ( error ) {
2015-07-29 13:52:59 +02:00
if ( error ) return updateError ( error ) ;
2015-07-20 00:09:47 -07:00
// fetch a signed sourceTarballUrl
superagent . get ( config . apiServerOrigin ( ) + '/api/v1/boxes/' + config . fqdn ( ) + '/sourcetarballurl' )
. query ( { token : config . token ( ) , boxVersion : boxUpdateInfo . version } )
. end ( function ( error , result ) {
2015-12-15 09:12:52 -08:00
if ( error && ! error . response ) return updateError ( new Error ( 'Network error fetching sourceTarballUrl: ' + error ) ) ;
if ( result . statusCode !== 200 ) return updateError ( new Error ( 'Error fetching sourceTarballUrl status: ' + result . statusCode ) ) ;
2015-09-28 23:10:09 -07:00
if ( ! safe . query ( result , 'body.url' ) ) return updateError ( new Error ( 'Error fetching sourceTarballUrl response: ' + JSON . stringify ( result . body ) ) ) ;
2015-07-20 00:09:47 -07:00
2015-08-26 10:59:17 -07:00
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
2015-07-20 00:09:47 -07:00
var args = {
2015-08-26 10:59:17 -07:00
sourceTarballUrl : result . body . url ,
// this data is opaque to the installer
data : {
2015-11-07 00:26:12 -08:00
token : config . token ( ) ,
2015-08-26 10:59:17 -07:00
apiServerOrigin : config . apiServerOrigin ( ) ,
2015-11-07 00:26:12 -08:00
webServerOrigin : config . webServerOrigin ( ) ,
2015-08-26 10:59:17 -07:00
fqdn : config . fqdn ( ) ,
2015-09-09 12:37:09 +02:00
tlsCert : fs . readFileSync ( path . join ( paths . NGINX _CERT _DIR , 'host.cert' ) , 'utf8' ) ,
tlsKey : fs . readFileSync ( path . join ( paths . NGINX _CERT _DIR , 'host.key' ) , 'utf8' ) ,
2015-11-07 00:26:12 -08:00
isCustomDomain : config . isCustomDomain ( ) ,
2015-11-12 14:22:43 -08:00
appstore : {
token : config . token ( ) ,
apiServerOrigin : config . apiServerOrigin ( )
} ,
caas : {
token : config . token ( ) ,
apiServerOrigin : config . apiServerOrigin ( ) ,
webServerOrigin : config . webServerOrigin ( )
} ,
2015-09-09 12:37:09 +02:00
version : boxUpdateInfo . version ,
2015-11-07 22:07:23 -08:00
boxVersionsUrl : config . get ( 'boxVersionsUrl' )
2015-08-26 10:59:17 -07:00
}
2015-07-20 00:09:47 -07:00
} ;
debug ( 'updating box %j' , args ) ;
superagent . post ( INSTALLER _UPDATE _URL ) . send ( args ) . end ( function ( error , result ) {
2015-12-15 09:12:52 -08:00
if ( error && ! error . response ) return updateError ( error ) ;
if ( result . statusCode !== 202 ) return updateError ( new Error ( 'Error initiating update: ' + JSON . stringify ( result . body ) ) ) ;
2015-07-20 00:09:47 -07:00
progress . set ( progress . UPDATE , 10 , 'Updating cloudron software' ) ;
callback ( null ) ;
} ) ;
} ) ;
// Do not add any code here. The installer script will stop the box code any instant
} ) ;
}
function backup ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
var error = locker . lock ( locker . OP _FULL _BACKUP ) ;
if ( error ) return callback ( new CloudronError ( CloudronError . BAD _STATE , error . message ) ) ;
2015-09-17 21:17:57 -07:00
// clearing backup ensures tools can 'wait' on progress
progress . clear ( progress . BACKUP ) ;
2015-07-20 00:09:47 -07:00
// start the backup operation in the background
backupBoxAndApps ( function ( error ) {
if ( error ) console . error ( 'backup failed.' , error ) ;
locker . unlock ( locker . OP _FULL _BACKUP ) ;
} ) ;
callback ( null ) ;
}
function ensureBackup ( callback ) {
callback = callback || function ( ) { } ;
backups . getAllPaged ( 1 , 1 , function ( error , backups ) {
if ( error ) {
debug ( 'Unable to list backups' , error ) ;
return callback ( error ) ; // no point trying to backup if appstore is down
}
if ( backups . length !== 0 && ( new Date ( ) - new Date ( backups [ 0 ] . creationTime ) < 23 * 60 * 60 * 1000 ) ) { // ~1 day ago
debug ( 'Previous backup was %j, no need to backup now' , backups [ 0 ] ) ;
return callback ( null ) ;
}
backup ( callback ) ;
} ) ;
}
function backupBoxWithAppBackupIds ( appBackupIds , callback ) {
assert ( util . isArray ( appBackupIds ) ) ;
2015-08-26 09:06:45 -07:00
backups . getBackupUrl ( null /* app */ , function ( error , result ) {
2015-08-25 10:23:24 -07:00
if ( error && error . reason === BackupsError . EXTERNAL _ERROR ) return callback ( new CloudronError ( CloudronError . EXTERNAL _ERROR , error . message ) ) ;
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2015-08-25 10:23:24 -07:00
debug ( 'backup: url %s' , result . url ) ;
2015-08-24 12:19:31 -07:00
2015-08-25 10:23:24 -07:00
async . series ( [
ignoreError ( shell . sudo . bind ( null , 'mountSwap' , [ BACKUP _SWAP _CMD , '--on' ] ) ) ,
2015-08-25 11:36:40 -07:00
shell . sudo . bind ( null , 'backupBox' , [ BACKUP _BOX _CMD , result . url , result . backupKey , result . sessionToken ] ) ,
2015-08-25 10:23:24 -07:00
ignoreError ( shell . sudo . bind ( null , 'unmountSwap' , [ BACKUP _SWAP _CMD , '--off' ] ) ) ,
] , function ( error ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2015-08-25 10:23:24 -07:00
debug ( 'backup: successful' ) ;
2015-08-24 12:19:31 -07:00
2015-08-25 10:23:24 -07:00
webhooks . backupDone ( result . id , null /* app */ , appBackupIds , function ( error ) {
if ( error ) return callback ( error ) ;
callback ( null , result . id ) ;
2015-08-24 12:19:31 -07:00
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}
// this function expects you to have a lock
function backupBox ( callback ) {
apps . getAll ( function ( error , allApps ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
var appBackupIds = allApps . map ( function ( app ) { return app . lastBackupId ; } ) ;
2015-07-24 06:53:07 -07:00
appBackupIds = appBackupIds . filter ( function ( id ) { return id !== null ; } ) ; // remove apps that were never backed up
2015-07-20 00:09:47 -07:00
backupBoxWithAppBackupIds ( appBackupIds , callback ) ;
} ) ;
}
// this function expects you to have a lock
function backupBoxAndApps ( callback ) {
callback = callback || function ( ) { } ; // callback can be empty for timer triggered backup
apps . getAll ( function ( error , allApps ) {
if ( error ) return callback ( new CloudronError ( CloudronError . INTERNAL _ERROR , error ) ) ;
var processed = 0 ;
var step = 100 / ( allApps . length + 1 ) ;
progress . set ( progress . BACKUP , processed , '' ) ;
async . mapSeries ( allApps , function iterator ( app , iteratorCallback ) {
++ processed ;
2015-07-20 00:50:36 -07:00
apps . backupApp ( app , app . manifest . addons , function ( error , backupId ) {
2015-09-21 14:34:25 -07:00
if ( error && error . reason !== AppsError . BAD _STATE ) {
debugApp ( app , 'Unable to backup' , error ) ;
return iteratorCallback ( error ) ;
2015-07-20 00:09:47 -07:00
}
2015-11-26 12:00:44 +01:00
progress . set ( progress . BACKUP , step * processed , 'Backed up app at ' + app . location ) ;
2015-09-21 14:34:25 -07:00
iteratorCallback ( null , backupId || null ) ; // clear backupId if is in BAD_STATE and never backed up
2015-07-20 00:09:47 -07:00
} ) ;
} , function appsBackedUp ( error , backupIds ) {
2015-07-28 14:40:22 -07:00
if ( error ) {
progress . set ( progress . BACKUP , 100 , error . message ) ;
return callback ( error ) ;
}
2015-07-20 00:09:47 -07:00
2015-09-21 14:34:25 -07:00
backupIds = backupIds . filter ( function ( id ) { return id !== null ; } ) ; // remove apps in bad state that were never backed up
2015-07-20 00:09:47 -07:00
backupBoxWithAppBackupIds ( backupIds , function ( error , restoreKey ) {
2015-07-28 14:40:22 -07:00
progress . set ( progress . BACKUP , 100 , error ? error . message : '' ) ;
2015-07-20 00:09:47 -07:00
callback ( error , restoreKey ) ;
} ) ;
} ) ;
} ) ;
}