'use strict'; exports = module.exports = { getServices, getService, getServicesConfig, configureService, getServiceLogs, restartService, rebuildService, startAppServices, stopAppServices, startServices, updateServiceConfig, setupAddons, teardownAddons, backupAddons, restoreAddons, clearAddons, getEnvironment, getMountsSync, getContainerNamesSync, getContainerDetails, SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck SERVICE_STATUS_ACTIVE: 'active', SERVICE_STATUS_STOPPED: 'stopped' }; var appdb = require('./appdb.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), crypto = require('crypto'), debug = require('debug')('box:addons'), docker = require('./docker.js'), fs = require('fs'), graphs = require('./graphs.js'), hat = require('./hat.js'), infra = require('./infra_version.js'), mail = require('./mail.js'), once = require('once'), os = require('os'), path = require('path'), paths = require('./paths.js'), rimraf = require('rimraf'), safe = require('safetydance'), semver = require('semver'), settings = require('./settings.js'), sftp = require('./sftp.js'), shell = require('./shell.js'), spawn = require('child_process').spawn, split = require('split'), request = require('request'), util = require('util'); const NOOP = function (app, options, callback) { return callback(); }; const NOOP_CALLBACK = function (error) { if (error) debug(error); }; const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh'); // setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost // teardown is destructive. app data stored with the addon is lost var ADDONS = { turn: { setup: setupTurn, teardown: teardownTurn, backup: NOOP, restore: NOOP, clear: NOOP }, email: { setup: setupEmail, teardown: teardownEmail, backup: NOOP, restore: setupEmail, clear: NOOP, }, ldap: { setup: setupLdap, teardown: teardownLdap, backup: NOOP, restore: setupLdap, clear: NOOP, }, localstorage: { setup: setupLocalStorage, teardown: teardownLocalStorage, backup: NOOP, // no backup because it's already inside app data restore: NOOP, clear: clearLocalStorage, }, mongodb: { setup: setupMongoDb, teardown: teardownMongoDb, backup: backupMongoDb, restore: restoreMongoDb, clear: clearMongodb, }, mysql: { setup: setupMySql, teardown: teardownMySql, backup: backupMySql, restore: restoreMySql, clear: clearMySql, }, postgresql: { setup: setupPostgreSql, teardown: teardownPostgreSql, backup: backupPostgreSql, restore: restorePostgreSql, clear: clearPostgreSql, }, recvmail: { setup: setupRecvMail, teardown: teardownRecvMail, backup: NOOP, restore: setupRecvMail, clear: NOOP, }, redis: { setup: setupRedis, teardown: teardownRedis, backup: backupRedis, restore: restoreRedis, clear: clearRedis, }, sendmail: { setup: setupSendMail, teardown: teardownSendMail, backup: NOOP, restore: setupSendMail, clear: NOOP, }, scheduler: { setup: NOOP, teardown: NOOP, backup: NOOP, restore: NOOP, clear: NOOP, }, docker: { setup: NOOP, teardown: NOOP, backup: NOOP, restore: NOOP, clear: NOOP, }, oauth: { // kept for backward compatibility. keep teardown for uninstall to work setup: NOOP, teardown: teardownOauth, backup: NOOP, restore: NOOP, clear: NOOP, } }; // services are actual containers that are running. addons are the concepts requested by app const SERVICES = { turn: { status: statusTurn, restart: restartContainer.bind(null, 'turn'), defaultMemoryLimit: 256 * 1024 * 1024 }, mail: { status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'), restart: mail.restartMail, defaultMemoryLimit: Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256) * 1024 * 1024 }, mongodb: { status: containerStatus.bind(null, 'mongodb', 'CLOUDRON_MONGODB_TOKEN'), restart: restartContainer.bind(null, 'mongodb'), defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200 * 1024 * 1024 }, mysql: { status: containerStatus.bind(null, 'mysql', 'CLOUDRON_MYSQL_TOKEN'), restart: restartContainer.bind(null, 'mysql'), defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024 }, postgresql: { status: containerStatus.bind(null, 'postgresql', 'CLOUDRON_POSTGRESQL_TOKEN'), restart: restartContainer.bind(null, 'postgresql'), defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024 }, docker: { status: statusDocker, restart: restartDocker, defaultMemoryLimit: 0 }, unbound: { status: statusUnbound, restart: restartUnbound, defaultMemoryLimit: 0 }, sftp: { status: statusSftp, restart: restartContainer.bind(null, 'sftp'), defaultMemoryLimit: 256 * 1024 * 1024 }, graphite: { status: statusGraphite, restart: restartContainer.bind(null, 'graphite'), defaultMemoryLimit: 75 * 1024 * 1024 }, nginx: { status: statusNginx, restart: restartNginx, defaultMemoryLimit: 0 } }; const APP_SERVICES = { redis: { status: (instance, done) => containerStatus(`redis-${instance}`, 'CLOUDRON_REDIS_TOKEN', done), start: (instance, done) => docker.startContainer(`redis-${instance}`, done), stop: (instance, done) => docker.stopContainer(`redis-${instance}`, done), restart: (instance, done) => restartContainer(`redis-${instance}`, done), defaultMemoryLimit: 150 * 1024 * 1024 } }; function debugApp(app /*, args */) { assert(typeof app === 'object'); debug((app.fqdn || app.location) + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); } function parseImageTag(tag) { let repository = tag.split(':', 1)[0]; let version = tag.substr(repository.length + 1).split('@', 1)[0]; let digest = tag.substr(repository.length + 1 + version.length + 1).split(':', 2)[1]; return { repository, version: semver.parse(version), digest }; } function requiresUpgrade(existingTag, currentTag) { let etag = parseImageTag(existingTag), ctag = parseImageTag(currentTag); return etag.version.major !== ctag.version.major; } // paths for dumps function dumpPath(addon, appId) { switch (addon) { case 'postgresql': return path.join(paths.APPS_DATA_DIR, appId, 'postgresqldump'); case 'mysql': return path.join(paths.APPS_DATA_DIR, appId, 'mysqldump'); case 'mongodb': return path.join(paths.APPS_DATA_DIR, appId, 'mongodbdump'); case 'redis': return path.join(paths.APPS_DATA_DIR, appId, 'dump.rdb'); } } function rebuildService(serviceName, callback) { assert.strictEqual(typeof serviceName, 'string'); assert.strictEqual(typeof callback, 'function'); // this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged // passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad if (serviceName === 'turn') return startTurn({ version: 'none' }, callback); if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback); if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback); if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback); if (serviceName === 'sftp') return sftp.startSftp({ version: 'none' }, callback); if (serviceName === 'graphite') return graphs.startGraphite({ version: 'none' }, callback); // nothing to rebuild for now callback(); } function restartContainer(name, callback) { assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); docker.restartContainer(name, function (error) { if (error && error.reason === BoxError.NOT_FOUND) { callback(null); // callback early since rebuilding takes long return rebuildService(name, function (error) { if (error) debug(`restartContainer: Unable to rebuild service ${name}`, error); }); } if (error) return callback(error); callback(error); }); } function getContainerDetails(containerName, tokenEnvName, callback) { assert.strictEqual(typeof containerName, 'string'); assert.strictEqual(typeof tokenEnvName, 'string'); assert.strictEqual(typeof callback, 'function'); docker.inspect(containerName, function (error, result) { if (error) return callback(error); const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null); if (!ip) return callback(new BoxError(BoxError.INACTIVE, `Error getting IP of ${containerName} service`)); // extract the cloudron token for auth const env = safe.query(result, 'Config.Env', null); if (!env) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error inspecting environment of ${containerName} service`)); const tmp = env.find(function (e) { return e.indexOf(tokenEnvName) === 0; }); if (!tmp) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error getting token of ${containerName} service`)); const token = tmp.slice(tokenEnvName.length + 1); // +1 for the = sign if (!token) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error getting token of ${containerName} service`)); callback(null, { ip: ip, token: token, state: result.State }); }); } function containerStatus(containerName, tokenEnvName, callback) { assert.strictEqual(typeof containerName, 'string'); assert.strictEqual(typeof tokenEnvName, 'string'); assert.strictEqual(typeof callback, 'function'); getContainerDetails(containerName, tokenEnvName, function (error, addonDetails) { if (error && (error.reason === BoxError.NOT_FOUND || error.reason === BoxError.INACTIVE)) return callback(null, { status: exports.SERVICE_STATUS_STOPPED }); if (error) return callback(error); request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}: ${error.message}` }); if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}` }); docker.memoryUsage(containerName, function (error, result) { if (error) return callback(error); var tmp = { status: addonDetails.state.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED, memoryUsed: result.memory_stats.usage, memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit) }; callback(null, tmp); }); }); }); } function getServices(callback) { assert.strictEqual(typeof callback, 'function'); let services = Object.keys(SERVICES); appdb.getAll(function (error, apps) { if (error) return callback(error); for (let app of apps) { if (app.manifest.addons && app.manifest.addons['redis']) services.push(`redis:${app.id}`); } callback(null, services); }); } function getServicesConfig(id, callback) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof callback, 'function'); const [name, instance ] = id.split(':'); if (!instance) { settings.getPlatformConfig(function (error, platformConfig) { if (error) return callback(error); callback(null, SERVICES[name], platformConfig); }); return; } appdb.get(instance, function (error, app) { if (error) return callback(error); callback(null, APP_SERVICES[name], app.servicesConfig); }); } function getService(id, callback) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof callback, 'function'); const [name, instance ] = id.split(':'); let containerStatusFunc; if (instance) { if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND)); containerStatusFunc = APP_SERVICES[name].status.bind(null, instance); } else if (SERVICES[name]) { containerStatusFunc = SERVICES[name].status; } else { return callback(new BoxError(BoxError.NOT_FOUND)); } var tmp = { name: name, status: null, memoryUsed: 0, memoryPercent: 0, error: null, config: { // If a property is not set then we cannot change it through the api, see below // memory: 0, // memorySwap: 0 } }; containerStatusFunc(function (error, result) { if (error) return callback(error); tmp.status = result.status; tmp.memoryUsed = result.memoryUsed; tmp.memoryPercent = result.memoryPercent; tmp.error = result.error || null; getServicesConfig(id, function (error, service, servicesConfig) { if (error) return callback(error); const serviceConfig = servicesConfig[name]; tmp.config = Object.assign({}, serviceConfig); if ((!tmp.config.memory || !tmp.config.memorySwap) && service.defaultMemoryLimit) { tmp.config.memory = service.defaultMemoryLimit; tmp.config.memorySwap = tmp.config.memory * 2; } callback(null, tmp); }); }); } function configureService(id, data, callback) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof callback, 'function'); const [name, instance ] = id.split(':'); if (instance) { if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND)); } else if (!SERVICES[name]) { return callback(new BoxError(BoxError.NOT_FOUND)); } getServicesConfig(id, function (error, service, servicesConfig) { if (error) return callback(error); if (!servicesConfig[name]) servicesConfig[name] = {}; // if not specified we clear the entry and use defaults if (!data.memory || !data.memorySwap) { delete servicesConfig[name].memory; delete servicesConfig[name].memorySwap; } else { servicesConfig[name] = data; } if (instance) { appdb.update(instance, { servicesConfig }, function (error) { if (error) return callback(error); updateAppServiceConfig(name, instance, servicesConfig, callback); }); } else { settings.setPlatformConfig(servicesConfig, function (error) { if (error) return callback(error); callback(null); }); } }); } function getServiceLogs(id, options, callback) { assert.strictEqual(typeof id, 'string'); assert(options && typeof options === 'object'); assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof options.lines, 'number'); assert.strictEqual(typeof options.format, 'string'); assert.strictEqual(typeof options.follow, 'boolean'); const [name, instance ] = id.split(':'); if (instance) { if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND)); } else if (!SERVICES[name]) { return callback(new BoxError(BoxError.NOT_FOUND)); } debug(`Getting logs for ${name}`); var lines = options.lines, format = options.format || 'json', follow = options.follow; let cmd, args = []; // docker and unbound use journald if (name === 'docker' || name === 'unbound') { cmd = 'journalctl'; args.push('--lines=' + (lines === -1 ? 'all' : lines)); args.push(`--unit=${name}`); args.push('--no-pager'); args.push('--output=short-iso'); if (follow) args.push('--follow'); } else if (name === 'nginx') { cmd = '/usr/bin/tail'; args.push('--lines=' + (lines === -1 ? '+1' : lines)); if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs args.push('/var/log/nginx/access.log'); args.push('/var/log/nginx/error.log'); } else { cmd = '/usr/bin/tail'; args.push('--lines=' + (lines === -1 ? '+1' : lines)); if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs const containerName = APP_SERVICES[name] ? `${name}-${instance}` : name; args.push(path.join(paths.LOG_DIR, containerName, 'app.log')); } var cp = spawn(cmd, args); var transformStream = split(function mapper(line) { if (format !== 'json') return line + '\n'; var data = line.split(' '); // logs are var timestamp = (new Date(data[0])).getTime(); if (isNaN(timestamp)) timestamp = 0; var message = line.slice(data[0].length+1); // ignore faulty empty logs if (!timestamp && !message) return; return JSON.stringify({ realtimeTimestamp: timestamp * 1000, message: message, source: name }) + '\n'; }); transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process cp.stdout.pipe(transformStream); callback(null, transformStream); } function restartService(id, callback) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof callback, 'function'); const [name, instance ] = id.split(':'); if (instance) { if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND)); APP_SERVICES[name].restart(instance, callback); } else if (SERVICES[name]) { SERVICES[name].restart(callback); } else { return callback(new BoxError(BoxError.NOT_FOUND)); } } // in the future, we can refcount and lazy start global services function startAppServices(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); const instance = app.id; async.eachSeries(Object.keys(app.manifest.addons || {}), function (addon, iteratorDone) { if (!(addon in APP_SERVICES)) return iteratorDone(); APP_SERVICES[addon].start(instance, function (error) { // assume addons name is service name // error ignored because we don't want "start app" to error. use can fix it from Services if (error) debug(`startAppServices: ${addon}:${instance}`, error); iteratorDone(); }); }, callback); } // in the future, we can refcount and stop global services as well function stopAppServices(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); const instance = app.id; async.eachSeries(Object.keys(app.manifest.addons || {}), function (addon, iteratorDone) { if (!(addon in APP_SERVICES)) return iteratorDone(); APP_SERVICES[addon].stop(instance, function (error) { // assume addons name is service name // error ignored because we don't want "start app" to error. use can fix it from Services if (error) debug(`stopAppServices: ${addon}:${instance}`, error); iteratorDone(); }); }, callback); } function waitForContainer(containerName, tokenEnvName, callback) { assert.strictEqual(typeof containerName, 'string'); assert.strictEqual(typeof tokenEnvName, 'string'); assert.strictEqual(typeof callback, 'function'); debug(`Waiting for ${containerName}`); getContainerDetails(containerName, tokenEnvName, function (error, result) { if (error) return callback(error); async.retry({ times: 10, interval: 15000 }, function (retryCallback) { request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Network error waiting for ${containerName}: ${error.message}`)); if (response.statusCode !== 200 || !response.body.status) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`)); retryCallback(null); }); }, callback); }); } function setupAddons(app, addons, callback) { assert.strictEqual(typeof app, 'object'); assert(!addons || typeof addons === 'object'); assert.strictEqual(typeof callback, 'function'); if (!addons) return callback(null); debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) { if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]); ADDONS[addon].setup(app, addons[addon], iteratorCallback); }, callback); } function teardownAddons(app, addons, callback) { assert.strictEqual(typeof app, 'object'); assert(!addons || typeof addons === 'object'); assert.strictEqual(typeof callback, 'function'); if (!addons) return callback(null); debugApp(app, 'teardownAddons: Tearing down %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) { if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]); ADDONS[addon].teardown(app, addons[addon], iteratorCallback); }, callback); } function backupAddons(app, addons, callback) { assert.strictEqual(typeof app, 'object'); assert(!addons || typeof addons === 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'backupAddons'); if (!addons) return callback(null); debugApp(app, 'backupAddons: Backing up %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); ADDONS[addon].backup(app, addons[addon], iteratorCallback); }, callback); } function clearAddons(app, addons, callback) { assert.strictEqual(typeof app, 'object'); assert(!addons || typeof addons === 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'clearAddons'); if (!addons) return callback(null); debugApp(app, 'clearAddons: clearing %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); ADDONS[addon].clear(app, addons[addon], iteratorCallback); }, callback); } function restoreAddons(app, addons, callback) { assert.strictEqual(typeof app, 'object'); assert(!addons || typeof addons === 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'restoreAddons'); if (!addons) return callback(null); debugApp(app, 'restoreAddons: restoring %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); ADDONS[addon].restore(app, addons[addon], iteratorCallback); }, callback); } function importAppDatabase(app, addon, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof addon, 'string'); assert.strictEqual(typeof callback, 'function'); if (!(addon in ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); async.series([ ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]), ADDONS[addon].clear.bind(null, app, app.manifest.addons[addon]), // clear in case we crashed in a restore ADDONS[addon].restore.bind(null, app, app.manifest.addons[addon]) ], callback); } function importDatabase(addon, callback) { assert.strictEqual(typeof addon, 'string'); assert.strictEqual(typeof callback, 'function'); debug(`importDatabase: Importing ${addon}`); appdb.getAll(function (error, allApps) { if (error) return callback(error); async.eachSeries(allApps, function iterator (app, iteratorCallback) { if (!(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon debug(`importDatabase: Importing addon ${addon} of app ${app.id}`); importAppDatabase(app, addon, function (error) { if (!error) return iteratorCallback(); debug(`importDatabase: Error importing ${addon} of app ${app.id}. Marking as errored`, error); // FIXME: there is no way to 'repair' if we are here. we need to make a separate apptask that re-imports db // not clear, if repair workflow should be part of addon or per-app appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback); }); }, function (error) { safe.fs.unlinkSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`)); // clean up for future migrations callback(error); }); }); } function exportDatabase(addon, callback) { assert.strictEqual(typeof addon, 'string'); assert.strictEqual(typeof callback, 'function'); debug(`exportDatabase: Exporting ${addon}`); if (fs.existsSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`))) { debug(`exportDatabase: Already exported addon ${addon} in previous run`); return callback(null); } appdb.getAll(function (error, apps) { if (error) return callback(error); async.eachSeries(apps, function iterator (app, iteratorCallback) { if (!app.manifest.addons || !(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon debug(`exportDatabase: Exporting addon ${addon} of app ${app.id}`); ADDONS[addon].backup(app, app.manifest.addons[addon], function (error) { if (error) { debug(`exportDatabase: Error exporting ${addon} of app ${app.id}.`, error); return iteratorCallback(error); } iteratorCallback(); }); }, function (error) { if (error) return callback(error); async.series([ (done) => fs.writeFile(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`), '', 'utf8', done), // note: after this point, we are restart safe. it's ok if the box code crashes at this point (done) => shell.exec(`exportDatabase - remove${addon}`, `docker rm -f ${addon}`, done), // what if db writes something when quitting ... (done) => shell.sudo(`exportDatabase - removeAddonDir${addon}`, [ RMADDONDIR_CMD, addon ], {}, done) // ready to start afresh ], callback); }); }); } function updateServiceConfig(platformConfig, callback) { assert.strictEqual(typeof platformConfig, 'object'); assert.strictEqual(typeof callback, 'function'); async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb', 'graphite' ], function iterator(serviceName, iteratorCallback) { const containerConfig = platformConfig[serviceName]; let memory, memorySwap; if (containerConfig && containerConfig.memory && containerConfig.memorySwap) { memory = containerConfig.memory; memorySwap = containerConfig.memorySwap; } else { memory = SERVICES[serviceName].defaultMemoryLimit; memorySwap = memory * 2; } const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.split(' '); // scale back db containers, if possible. this is retried because updating memory constraints can fail // with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy async.retry({ times: 10, interval: 60 * 1000 }, function (retryCallback) { shell.spawn(`updateServiceConfig(${serviceName})`, '/usr/bin/docker', args, { }, retryCallback); }, iteratorCallback); }, callback); } function updateAppServiceConfig(name, instance, servicesConfig, callback) { assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof instance, 'string'); assert.strictEqual(typeof servicesConfig, 'object'); assert.strictEqual(typeof callback, 'function'); debug(`updateAppServiceConfig: ${name}-${instance} ${JSON.stringify(servicesConfig)}`); const serviceConfig = servicesConfig[name]; let memory, memorySwap; if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) { memory = serviceConfig.memory; memorySwap = serviceConfig.memorySwap; } else { memory = APP_SERVICES[name].defaultMemoryLimit; memorySwap = memory * 2; } const args = `update --memory ${memory} --memory-swap ${memorySwap} ${name}-${instance}`.split(' '); shell.spawn(`updateAppServiceConfig${name}`, '/usr/bin/docker', args, { }, callback); } function startServices(existingInfra, callback) { assert.strictEqual(typeof existingInfra, 'object'); assert.strictEqual(typeof callback, 'function'); let startFuncs = [ ]; // always start addons on any infra change, regardless of minor or major update if (existingInfra.version !== infra.version) { debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`); startFuncs.push( startTurn.bind(null, existingInfra), startMysql.bind(null, existingInfra), startPostgresql.bind(null, existingInfra), startMongodb.bind(null, existingInfra), startRedis.bind(null, existingInfra), mail.startMail); } else { assert.strictEqual(typeof existingInfra.images, 'object'); if (infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra)); if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra)); if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra)); if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra)); if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail); if (infra.images.redis.tag !== existingInfra.images.redis.tag) startFuncs.push(startRedis.bind(null, existingInfra)); debug('startServices: existing infra. incremental service create %j', startFuncs.map(function (f) { return f.name; })); } async.series(startFuncs, callback); } function getEnvironment(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); appdb.getAddonConfigByAppId(app.id, function (error, result) { if (error) return callback(error); if (app.manifest.addons['docker']) result.push({ name: 'CLOUDRON_DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` }); return callback(null, result.map(function (e) { return e.name + '=' + e.value; })); }); } function getMountsSync(app, addons) { assert.strictEqual(typeof app, 'object'); assert(!addons || typeof addons === 'object'); let mounts = [ ]; if (!addons) return mounts; for (let addon in addons) { switch (addon) { case 'localstorage': mounts.push({ Target: '/app/data', Source: `${app.id}-localstorage`, Type: 'volume', ReadOnly: false }); break; default: break; } } return mounts; } function getContainerNamesSync(app, addons) { assert.strictEqual(typeof app, 'object'); assert(!addons || typeof addons === 'object'); var names = [ ]; if (!addons) return names; for (var addon in addons) { switch (addon) { case 'scheduler': // names here depend on how scheduler.js creates containers names = names.concat(Object.keys(addons.scheduler).map(function (taskName) { return app.id + '-' + taskName; })); break; default: break; } } return names; } function setupLocalStorage(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'setupLocalStorage'); const volumeDataDir = apps.getDataDir(app, app.dataDir); // reomve any existing volume in case it's bound with an old dataDir async.series([ docker.removeVolume.bind(null, `${app.id}-localstorage`), docker.createVolume.bind(null, `${app.id}-localstorage`, volumeDataDir, { fqdn: app.fqdn, appId: app.id }) ], callback); } function clearLocalStorage(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'clearLocalStorage'); docker.clearVolume(`${app.id}-localstorage`, { removeDirectory: false }, callback); } function teardownLocalStorage(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'teardownLocalStorage'); async.series([ docker.clearVolume.bind(null, `${app.id}-localstorage`, { removeDirectory: true }), docker.removeVolume.bind(null, `${app.id}-localstorage`) ], callback); } function setupTurn(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8'); if (!turnSecret) debug('setupTurn: no turn secret set. Will leave emtpy, but this is a problem!'); const env = [ { name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() }, { name: 'CLOUDRON_STUN_PORT', value: '3478' }, { name: 'CLOUDRON_STUN_TLS_PORT', value: '5349' }, { name: 'CLOUDRON_TURN_SERVER', value: settings.adminFqdn() }, { name: 'CLOUDRON_TURN_PORT', value: '3478' }, { name: 'CLOUDRON_TURN_TLS_PORT', value: '5349' }, { name: 'CLOUDRON_TURN_SECRET', value: turnSecret } ]; debugApp(app, 'Setting up TURN'); appdb.setAddonConfig(app.id, 'turn', env, callback); } function teardownTurn(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Tearing down TURN'); appdb.unsetAddonConfig(app.id, 'turn', callback); } function setupEmail(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); mail.getDomains(function (error, mailDomains) { if (error) return callback(error); const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(','); const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; // note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation) var env = [ { name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' }, { name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' }, { name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' }, { name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' }, { name: `${envPrefix}MAIL_SIEVE_SERVER`, value: 'mail' }, { name: `${envPrefix}MAIL_SIEVE_PORT`, value: '4190' }, { name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }, { name: `${envPrefix}MAIL_DOMAINS`, value: mailInDomains }, { name: 'CLOUDRON_MAIL_SERVER_HOST', value: settings.mailFqdn() }, { name: `${envPrefix}LDAP_MAILBOXES_BASE_DN`, value: 'ou=mailboxes,dc=cloudron' } ]; debugApp(app, 'Setting up Email'); appdb.setAddonConfig(app.id, 'email', env, callback); }); } function teardownEmail(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Tearing down Email'); appdb.unsetAddonConfig(app.id, 'email', callback); } function setupLdap(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); if (!app.sso) return callback(null); const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; var env = [ { name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' }, { name: 'CLOUDRON_LDAP_HOST', value: '172.18.0.1' }, // to keep things in sync with the database _HOST vars { name: `${envPrefix}LDAP_PORT`, value: '' + constants.LDAP_PORT }, { name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + constants.LDAP_PORT }, { name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' }, { name: `${envPrefix}LDAP_GROUPS_BASE_DN`, value: 'ou=groups,dc=cloudron' }, { name: `${envPrefix}LDAP_BIND_DN`, value: 'cn='+ app.id + ',ou=apps,dc=cloudron' }, { name: `${envPrefix}LDAP_BIND_PASSWORD`, value: hat(4 * 128) } // this is ignored ]; debugApp(app, 'Setting up LDAP'); appdb.setAddonConfig(app.id, 'ldap', env, callback); } function teardownLdap(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Tearing down LDAP'); appdb.unsetAddonConfig(app.id, 'ldap', callback); } function setupSendMail(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Setting up SendMail'); appdb.getAddonConfigByName(app.id, 'sendmail', '%MAIL_SMTP_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; var env = [ { name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' }, { name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' }, { name: `${envPrefix}MAIL_SMTPS_PORT`, value: '2465' }, { name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.mailboxDomain }, { name: `${envPrefix}MAIL_SMTP_PASSWORD`, value: password }, { name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.mailboxDomain }, { name: `${envPrefix}MAIL_DOMAIN`, value: app.mailboxDomain } ]; debugApp(app, 'Setting sendmail addon config to %j', env); appdb.setAddonConfig(app.id, 'sendmail', env, callback); }); } function teardownSendMail(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Tearing down sendmail'); appdb.unsetAddonConfig(app.id, 'sendmail', callback); } function setupRecvMail(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Setting up recvmail'); appdb.getAddonConfigByName(app.id, 'recvmail', '%MAIL_IMAP_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; var env = [ { name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' }, { name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' }, { name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.mailboxDomain }, { name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password }, { name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.mailboxDomain }, { name: `${envPrefix}MAIL_DOMAIN`, value: app.mailboxDomain } ]; debugApp(app, 'Setting sendmail addon config to %j', env); appdb.setAddonConfig(app.id, 'recvmail', env, callback); }); } function teardownRecvMail(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Tearing down recvmail'); appdb.unsetAddonConfig(app.id, 'recvmail', callback); } function mysqlDatabaseName(appId) { assert.strictEqual(typeof appId, 'string'); var md5sum = crypto.createHash('md5'); // get rid of "-" md5sum.update(appId); return md5sum.digest('hex').substring(0, 16); // max length of mysql usernames is 16 } function startMysql(existingInfra, callback) { assert.strictEqual(typeof existingInfra, 'object'); assert.strictEqual(typeof callback, 'function'); const tag = infra.images.mysql.tag; const dataDir = paths.PLATFORM_DATA_DIR; const rootPassword = hat(8 * 128); const cloudronToken = hat(8 * 128); const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag); if (upgrading) debug('startMysql: mysql will be upgraded'); const upgradeFunc = upgrading ? exportDatabase.bind(null, 'mysql') : (next) => next(); upgradeFunc(function (error) { if (error) return callback(error); // memory options are applied dynamically. import requires all the memory we can get const cmd = `docker run --restart=always -d --name="mysql" \ --hostname mysql \ --net cloudron \ --net-alias mysql \ --log-driver syslog \ --log-opt syslog-address=udp://127.0.0.1:2514 \ --log-opt syslog-format=rfc5424 \ --log-opt tag=mysql \ --dns 172.18.0.1 \ --dns-search=. \ -e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \ -e CLOUDRON_MYSQL_ROOT_HOST=172.18.0.1 \ -e CLOUDRON_MYSQL_ROOT_PASSWORD=${rootPassword} \ -v "${dataDir}/mysql:/var/lib/mysql" \ --label isCloudronManaged=true \ --read-only -v /tmp -v /run "${tag}"`; async.series([ shell.exec.bind(null, 'stopMysql', 'docker stop mysql || true'), shell.exec.bind(null, 'removeMysql', 'docker rm -f mysql || true'), shell.exec.bind(null, 'startMysql', cmd) ], function (error) { if (error) return callback(error); waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) { if (error) return callback(error); if (!upgrading) return callback(null); importDatabase('mysql', callback); }); }); }); } function setupMySql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Setting up mysql'); appdb.getAddonConfigByName(app.id, 'mysql', '%MYSQL_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); const tmp = mysqlDatabaseName(app.id); const data = { database: tmp, prefix: tmp, username: tmp, password: error ? hat(4 * 48) : existingPassword // see box#362 for password length }; getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) { if (error) return callback(error); request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mysql: ${error.message}`)); if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; var env = [ { name: `${envPrefix}MYSQL_USERNAME`, value: data.username }, { name: `${envPrefix}MYSQL_PASSWORD`, value: data.password }, { name: `${envPrefix}MYSQL_HOST`, value: 'mysql' }, { name: `${envPrefix}MYSQL_PORT`, value: '3306' } ]; if (options.multipleDatabases) { env = env.concat({ name: `${envPrefix}MYSQL_DATABASE_PREFIX`, value: `${data.prefix}_` }); } else { env = env.concat( { name: `${envPrefix}MYSQL_URL`, value: `mysql://${data.username}:${data.password}@mysql/${data.database}` }, { name: `${envPrefix}MYSQL_DATABASE`, value: data.database } ); } debugApp(app, 'Setting mysql addon config to %j', env); appdb.setAddonConfig(app.id, 'mysql', env, callback); }); }); }); } function clearMySql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); const database = mysqlDatabaseName(app.id); getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) { if (error) return callback(error); request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mysql: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(); }); }); } function teardownMySql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); const database = mysqlDatabaseName(app.id); const username = database; getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) { if (error) return callback(error); request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mysql: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); appdb.unsetAddonConfig(app.id, 'mysql', callback); }); }); } function pipeRequestToFile(url, filename, callback) { assert.strictEqual(typeof url, 'string'); assert.strictEqual(typeof filename, 'string'); assert.strictEqual(typeof callback, 'function'); const writeStream = fs.createWriteStream(filename); const done = once(function (error) { // the writeStream and the request can both error if (error) writeStream.close(); callback(error); }); writeStream.on('error', (error) => done(new BoxError(BoxError.FS_ERROR, `Error writing to ${filename}: ${error.message}`))); writeStream.on('open', function () { // note: do not attach to post callback handler because this will buffer the entire reponse! // see https://github.com/request/request/issues/2270 const req = request.post(url, { rejectUnauthorized: false }); req.on('error', (error) => done(new BoxError(BoxError.NETWORK_ERROR, `Request error writing to ${filename}: ${error.message}`))); // network error, dns error, request errored in middle etc req.on('response', function (response) { if (response.statusCode !== 200) return done(new BoxError(BoxError.ADDONS_ERROR, `Unexpected response code when piping ${url}: ${response.statusCode} message: ${response.statusMessage} filename: ${filename}`)); response.pipe(writeStream).on('finish', done); // this is hit after data written to disk }); }); } function backupMySql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); const database = mysqlDatabaseName(app.id); debugApp(app, 'Backing up mysql'); getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) { if (error) return callback(error); const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`; pipeRequestToFile(url, dumpPath('mysql', app.id), callback); }); } function restoreMySql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); const database = mysqlDatabaseName(app.id); debugApp(app, 'restoreMySql'); callback = once(callback); // protect from multiple returns with streams getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) { if (error) return callback(error); var input = fs.createReadStream(dumpPath('mysql', app.id)); input.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring mysql: ${error.message}`))); const restoreReq = request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mysql: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); input.pipe(restoreReq); }); } function postgreSqlNames(appId) { appId = appId.replace(/-/g, ''); return { database: `db${appId}`, username: `user${appId}` }; } function startPostgresql(existingInfra, callback) { assert.strictEqual(typeof existingInfra, 'object'); assert.strictEqual(typeof callback, 'function'); const tag = infra.images.postgresql.tag; const dataDir = paths.PLATFORM_DATA_DIR; const rootPassword = hat(8 * 128); const cloudronToken = hat(8 * 128); const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag); if (upgrading) debug('startPostgresql: postgresql will be upgraded'); const upgradeFunc = upgrading ? exportDatabase.bind(null, 'postgresql') : (next) => next(); upgradeFunc(function (error) { if (error) return callback(error); // memory options are applied dynamically. import requires all the memory we can get const cmd = `docker run --restart=always -d --name="postgresql" \ --hostname postgresql \ --net cloudron \ --net-alias postgresql \ --log-driver syslog \ --log-opt syslog-address=udp://127.0.0.1:2514 \ --log-opt syslog-format=rfc5424 \ --log-opt tag=postgresql \ --dns 172.18.0.1 \ --dns-search=. \ -e CLOUDRON_POSTGRESQL_ROOT_PASSWORD="${rootPassword}" \ -e CLOUDRON_POSTGRESQL_TOKEN="${cloudronToken}" \ -v "${dataDir}/postgresql:/var/lib/postgresql" \ --label isCloudronManaged=true \ --read-only -v /tmp -v /run "${tag}"`; async.series([ shell.exec.bind(null, 'stopPostgresql', 'docker stop postgresql || true'), shell.exec.bind(null, 'removePostgresql', 'docker rm -f postgresql || true'), shell.exec.bind(null, 'startPostgresql', cmd) ], function (error) { if (error) return callback(error); waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) { if (error) return callback(error); if (!upgrading) return callback(null); importDatabase('postgresql', callback); }); }); }); } function setupPostgreSql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Setting up postgresql'); const { database, username } = postgreSqlNames(app.id); appdb.getAddonConfigByName(app.id, 'postgresql', '%POSTGRESQL_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); const data = { database: database, username: username, password: error ? hat(4 * 128) : existingPassword, locale: options.locale || 'C' }; getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) { if (error) return callback(error); request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up postgresql: ${error.message}`)); if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; var env = [ { name: `${envPrefix}POSTGRESQL_URL`, value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` }, { name: `${envPrefix}POSTGRESQL_USERNAME`, value: data.username }, { name: `${envPrefix}POSTGRESQL_PASSWORD`, value: data.password }, { name: `${envPrefix}POSTGRESQL_HOST`, value: 'postgresql' }, { name: `${envPrefix}POSTGRESQL_PORT`, value: '5432' }, { name: `${envPrefix}POSTGRESQL_DATABASE`, value: data.database } ]; debugApp(app, 'Setting postgresql addon config to %j', env); appdb.setAddonConfig(app.id, 'postgresql', env, callback); }); }); }); } function clearPostgreSql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); const { database, username } = postgreSqlNames(app.id); const locale = options.locale || 'C'; debugApp(app, 'Clearing postgresql'); getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) { if (error) return callback(error); request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}&locale=${locale}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing postgresql: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); }); } function teardownPostgreSql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); const { database, username } = postgreSqlNames(app.id); getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) { if (error) return callback(error); request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error tearing down postgresql: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); appdb.unsetAddonConfig(app.id, 'postgresql', callback); }); }); } function backupPostgreSql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Backing up postgresql'); const { database } = postgreSqlNames(app.id); getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) { if (error) return callback(error); const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`; pipeRequestToFile(url, dumpPath('postgresql', app.id), callback); }); } function restorePostgreSql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Restore postgresql'); const { database, username } = postgreSqlNames(app.id); callback = once(callback); // protect from multiple returns with streams getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) { if (error) return callback(error); var input = fs.createReadStream(dumpPath('postgresql', app.id)); input.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring postgresql: ${error.message}`))); const restoreReq = request.post(`https://${result.ip}:3000/databases/${database}/restore?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring postgresql: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); input.pipe(restoreReq); }); } function startTurn(existingInfra, callback) { assert.strictEqual(typeof existingInfra, 'object'); assert.strictEqual(typeof callback, 'function'); // get and ensure we have a turn secret var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8'); if (!turnSecret) { turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter safe.fs.writeFileSync(paths.ADDON_TURN_SECRET_FILE, turnSecret, 'utf8'); } const tag = infra.images.turn.tag; const memoryLimit = 256; const realm = settings.adminFqdn(); // this exports 3478/tcp, 5349/tls and 50000-51000/udp const cmd = `docker run --restart=always -d --name="turn" \ --hostname turn \ --net host \ --log-driver syslog \ --log-opt syslog-address=udp://127.0.0.1:2514 \ --log-opt syslog-format=rfc5424 \ --log-opt tag=turn \ -m ${memoryLimit}m \ --memory-swap ${memoryLimit * 2}m \ --dns 172.18.0.1 \ --dns-search=. \ -e CLOUDRON_TURN_SECRET="${turnSecret}" \ -e CLOUDRON_REALM="${realm}" \ --label isCloudronManaged=true \ --read-only -v /tmp -v /run "${tag}"`; async.series([ shell.exec.bind(null, 'stopTurn', 'docker stop turn || true'), shell.exec.bind(null, 'removeTurn', 'docker rm -f turn || true'), shell.exec.bind(null, 'startTurn', cmd) ], callback); } function startMongodb(existingInfra, callback) { assert.strictEqual(typeof existingInfra, 'object'); assert.strictEqual(typeof callback, 'function'); const tag = infra.images.mongodb.tag; const dataDir = paths.PLATFORM_DATA_DIR; const rootPassword = hat(8 * 128); const cloudronToken = hat(8 * 128); const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag); if (upgrading) debug('startMongodb: mongodb will be upgraded'); const upgradeFunc = upgrading ? exportDatabase.bind(null, 'mongodb') : (next) => next(); upgradeFunc(function (error) { if (error) return callback(error); // memory options are applied dynamically. import requires all the memory we can get const cmd = `docker run --restart=always -d --name="mongodb" \ --hostname mongodb \ --net cloudron \ --net-alias mongodb \ --log-driver syslog \ --log-opt syslog-address=udp://127.0.0.1:2514 \ --log-opt syslog-format=rfc5424 \ --log-opt tag=mongodb \ --dns 172.18.0.1 \ --dns-search=. \ -e CLOUDRON_MONGODB_ROOT_PASSWORD="${rootPassword}" \ -e CLOUDRON_MONGODB_TOKEN="${cloudronToken}" \ -v "${dataDir}/mongodb:/var/lib/mongodb" \ --label isCloudronManaged=true \ --read-only -v /tmp -v /run "${tag}"`; async.series([ shell.exec.bind(null, 'stopMongodb', 'docker stop mongodb || true'), shell.exec.bind(null, 'removeMongodb', 'docker rm -f mongodb || true'), shell.exec.bind(null, 'startMongodb', cmd) ], function (error) { if (error) return callback(error); waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) { if (error) return callback(error); if (!upgrading) return callback(null); importDatabase('mongodb', callback); }); }); }); } function setupMongoDb(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Setting up mongodb'); appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { database = database || hat(8 * 8); // 16 bytes. keep this short, so as to not overflow the 127 byte index length in MongoDB < 4.4 const data = { database: database, username: app.id, password: error ? hat(4 * 128) : existingPassword, oplog: !!options.oplog }; getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mongodb: ${error.message}`)); if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; var env = [ { name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/${data.database}` }, { name: `${envPrefix}MONGODB_USERNAME`, value : data.username }, { name: `${envPrefix}MONGODB_PASSWORD`, value: data.password }, { name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' }, { name: `${envPrefix}MONGODB_PORT`, value : '27017' }, { name: `${envPrefix}MONGODB_DATABASE`, value : data.database } ]; if (options.oplog) { env.push({ name: `${envPrefix}MONGODB_OPLOG_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/local?authSource=${data.database}` }); } debugApp(app, 'Setting mongodb addon config to %j', env); appdb.setAddonConfig(app.id, 'mongodb', env, callback); }); }); }); }); } function clearMongodb(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { if (error) return callback(error); request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mongodb: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(); }); }); }); } function teardownMongoDb(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null); if (error) return callback(error); request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); appdb.unsetAddonConfig(app.id, 'mongodb', callback); }); }); }); } function backupMongoDb(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Backing up mongodb'); getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { if (error) return callback(error); const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`; pipeRequestToFile(url, dumpPath('mongodb', app.id), callback); }); }); } function restoreMongoDb(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); callback = once(callback); // protect from multiple returns with streams debugApp(app, 'restoreMongoDb'); getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { if (error) return callback(error); const readStream = fs.createReadStream(dumpPath('mongodb', app.id)); readStream.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring mongodb: ${error.message}`))); const restoreReq = request.post(`https://${result.ip}:3000/databases/${database}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); readStream.pipe(restoreReq); }); }); } function startRedis(existingInfra, callback) { assert.strictEqual(typeof existingInfra, 'object'); assert.strictEqual(typeof callback, 'function'); const tag = infra.images.redis.tag; const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.redis.tag, tag); apps.getAll(function (error, allApps) { if (error) return callback(error); async.eachSeries(allApps, function iterator (app, iteratorCallback) { if (!('redis' in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon const redisName = 'redis-' + app.id; async.series([ shell.exec.bind(null, 'stopRedis', `docker stop ${redisName} || true`), // redis will backup as part of signal handling shell.exec.bind(null, 'removeRedis', `docker rm -f ${redisName} || true`), setupRedis.bind(null, app, app.manifest.addons.redis) // starts the container ], iteratorCallback); }, function (error) { if (error) return callback(error); if (!upgrading) return callback(); importDatabase('redis', callback); }); }); } // Ensures that app's addon redis container is running. Can be called when named container already exists/running function setupRedis(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); const redisName = 'redis-' + app.id; appdb.getAddonConfigByName(app.id, 'redis', '%REDIS_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); const redisPassword = options.noPassword ? '' : (error ? hat(4 * 48) : existingPassword); // see box#362 for password length const redisServiceToken = hat(4 * 48); // Compute redis memory limit based on app's memory limit (this is arbitrary) const memoryLimit = app.servicesConfig['redis'] ? app.servicesConfig['redis'].memory : APP_SERVICES['redis'].defaultMemoryLimit; const tag = infra.images.redis.tag; const label = app.fqdn; // note that we do not add appId label because this interferes with the stop/start app logic const cmd = `docker run --restart=always -d --name=${redisName} \ --hostname ${redisName} \ --label=location=${label} \ --net cloudron \ --net-alias ${redisName} \ --log-driver syslog \ --log-opt syslog-address=udp://127.0.0.1:2514 \ --log-opt syslog-format=rfc5424 \ --log-opt tag="${redisName}" \ -m ${memoryLimit/2} \ --memory-swap ${memoryLimit} \ --dns 172.18.0.1 \ --dns-search=. \ -e CLOUDRON_REDIS_PASSWORD="${redisPassword}" \ -e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \ -v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \ --label isCloudronManaged=true \ --read-only -v /tmp -v /run ${tag}`; const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; var env = [ { name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id }, { name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword }, { name: `${envPrefix}REDIS_HOST`, value: redisName }, { name: `${envPrefix}REDIS_PORT`, value: '6379' } ]; async.series([ (next) => { docker.inspect(redisName, function (inspectError, result) { // fast-path if (!inspectError) { debug(`Re-using existing redis container with state: ${JSON.stringify(result.State)}`); return next(); } shell.exec('startRedis', cmd, next); }); }, appdb.setAddonConfig.bind(null, app.id, 'redis', env), waitForContainer.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN') ], function (error) { if (error) debug('Error setting up redis: ', error); callback(error); }); }); } function clearRedis(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Clearing redis'); getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) { if (error) return callback(error); request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing redis: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing redis. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); }); } function teardownRedis(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); docker.deleteContainer(`redis-${app.id}`, function (error) { if (error) return callback(error); shell.sudo('removeVolume', [ RMADDONDIR_CMD, 'redis', app.id ], {}, function (error) { if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error removing redis data: ${error.message}`)); rimraf(path.join(paths.LOG_DIR, `redis-${app.id}`), function (error) { if (error) debugApp(app, 'cannot cleanup logs: %s', error); appdb.unsetAddonConfig(app.id, 'redis', callback); }); }); }); } function backupRedis(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Backing up redis'); getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) { if (error) return callback(error); const url = `https://${result.ip}:3000/backup?access_token=${result.token}`; pipeRequestToFile(url, dumpPath('redis', app.id), callback); }); } function restoreRedis(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'Restoring redis'); callback = once(callback); // protect from multiple returns with streams getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) { if (error) return callback(error); let input; const newDumpLocation = dumpPath('redis', app.id); if (fs.existsSync(newDumpLocation)) { input = fs.createReadStream(newDumpLocation); } else { // old location of dumps input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'redis/dump.rdb')); } input.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring redis: ${error.message}`))); const restoreReq = request.post(`https://${result.ip}:3000/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring redis: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring redis. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); input.pipe(restoreReq); }); } function statusTurn(callback) { assert.strictEqual(typeof callback, 'function'); docker.inspect('turn', function (error, container) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED }); if (error) return callback(error); docker.memoryUsage(container.Id, function (error, result) { if (error) return callback(error); var tmp = { status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED, memoryUsed: result.memory_stats.usage, memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit) }; callback(null, tmp); }); }); } function statusDocker(callback) { assert.strictEqual(typeof callback, 'function'); docker.ping(function (error) { callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED: exports.SERVICE_STATUS_ACTIVE }); }); } function restartDocker(callback) { assert.strictEqual(typeof callback, 'function'); shell.sudo('restartdocker', [ path.join(__dirname, 'scripts/restartdocker.sh') ], {}, NOOP_CALLBACK); callback(null); } function statusUnbound(callback) { assert.strictEqual(typeof callback, 'function'); shell.exec('statusUnbound', 'systemctl is-active unbound', function (error) { callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE }); }); } function restartUnbound(callback) { assert.strictEqual(typeof callback, 'function'); shell.sudo('restartunbound', [ path.join(__dirname, 'scripts/restartunbound.sh') ], {}, NOOP_CALLBACK); callback(null); } function statusNginx(callback) { assert.strictEqual(typeof callback, 'function'); shell.exec('statusNginx', 'systemctl is-active nginx', function (error) { callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE }); }); } function restartNginx(callback) { assert.strictEqual(typeof callback, 'function'); shell.sudo('reloadnginx', [ path.join(__dirname, 'scripts/reloadnginx.sh') ], {}, NOOP_CALLBACK); callback(null); } function statusSftp(callback) { assert.strictEqual(typeof callback, 'function'); docker.inspect('sftp', function (error, container) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED }); if (error) return callback(error); docker.memoryUsage('sftp', function (error, result) { if (error) return callback(error); var tmp = { status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED, memoryUsed: result.memory_stats.usage, memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit) }; callback(null, tmp); }); }); } function statusGraphite(callback) { assert.strictEqual(typeof callback, 'function'); docker.inspect('graphite', function (error, container) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED }); if (error) return callback(error); request.get('http://127.0.0.1:8417/graphite-web/dashboard', { json: true, timeout: 3000 }, function (error, response) { if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite: ${error.message}` }); if (response.statusCode !== 200) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite. Status code: ${response.statusCode} message: ${response.body.message}` }); docker.memoryUsage('graphite', function (error, result) { if (error) return callback(error); var tmp = { status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED, memoryUsed: result.memory_stats.usage, memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit) }; callback(null, tmp); }); }); }); } function teardownOauth(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); debugApp(app, 'teardownOauth'); appdb.unsetAddonConfig(app.id, 'oauth', callback); }