diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f914a6a0b..d2a07a2a5 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2030,6 +2030,11 @@ } } }, + "node-df": { + "version": "0.1.1", + "from": "node-df@*", + "resolved": "https://registry.npmjs.org/node-df/-/node-df-0.1.1.tgz" + }, "node-uuid": { "version": "1.4.7", "from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", diff --git a/package.json b/package.json index 458fd88b4..257c89822 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "multiparty": "^4.1.2", "mysql": "^2.7.0", "native-dns": "^0.7.0", + "node-df": "^0.1.1", "node-uuid": "^1.4.3", "nodemailer": "^1.3.0", "nodemailer-smtp-transport": "^1.0.3", diff --git a/src/cloudron.js b/src/cloudron.js index e8ce785b2..34a62b30c 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -22,6 +22,8 @@ exports = module.exports = { isConfiguredSync: isConfiguredSync, + checkDiskSpace: checkDiskSpace, + events: new (require('events').EventEmitter)(), EVENT_ACTIVATED: 'activated', @@ -37,8 +39,10 @@ var apps = require('./apps.js'), clientdb = require('./clientdb.js'), config = require('./config.js'), debug = require('debug')('box:cloudron'), + df = require('node-df'), fs = require('fs'), locker = require('./locker.js'), + mailer = require('./mailer.js'), os = require('os'), path = require('path'), paths = require('./paths.js'), @@ -742,3 +746,28 @@ function backupBoxAndApps(callback) { }); }); } + +function checkDiskSpace(callback) { + callback = callback || function () { }; // callback can be empty for timer triggered check + + debug('Checking disk space'); + + df(function (error, entries) { + if (error) { + debug('df error %s', error.message); + mailer.outOfDiskSpace(error.message); + return callback(); + } + + var oos = entries.some(function (entry) { + return (entry.mount === paths.DATA_DIR && entry.capacity >= 0.90) || + (entry.mount === '/' && entry.capacity >= 0.95); + }); + + debug('Disk space checked. ok: %s', !oos); + + if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4)); + + callback(); + }); +} diff --git a/src/cron.js b/src/cron.js index 5c44a3f1a..c0c3b0685 100644 --- a/src/cron.js +++ b/src/cron.js @@ -25,7 +25,8 @@ var gAutoupdaterJob = null, gCleanupTokensJob = null, gDockerVolumeCleanerJob = null, gSchedulerSyncJob = null, - gCertificateRenewJob = null; + gCertificateRenewJob = null, + gCheckDiskSpaceJob = null; var NOOP_CALLBACK = function (error) { if (error) console.error(error); }; @@ -69,6 +70,14 @@ function recreateJobs(unusedTimeZone, callback) { timeZone: allSettings[settings.TIME_ZONE_KEY] }); + if (gCheckDiskSpaceJob) gCheckDiskSpaceJob.stop(); + gCheckDiskSpaceJob = new CronJob({ + cronTime: '00 30 */4 * * *', // every 4 hours + onTick: cloudron.checkDiskSpace, + start: true, + timeZone: allSettings[settings.TIME_ZONE_KEY] + }); + if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop(); gBoxUpdateCheckerJob = new CronJob({ cronTime: '00 */10 * * * *', // every 10 minutes diff --git a/src/mail_templates/out_of_disk_space.ejs b/src/mail_templates/out_of_disk_space.ejs new file mode 100644 index 000000000..7e2c3d6ec --- /dev/null +++ b/src/mail_templates/out_of_disk_space.ejs @@ -0,0 +1,19 @@ +<%if (format === 'text') { %> + +Dear Cloudron Team, + +<%= fqdn %> is running out of disk space. + +Please see some excerpts of the logs below. + +Thank you, +Your Cloudron + +------------------------------------- + +<%- message %> + +<% } else { %> + +<% } %> + diff --git a/src/mailer.js b/src/mailer.js index 9b966adaf..05efc1f34 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -18,6 +18,8 @@ exports = module.exports = { appDied: appDied, + outOfDiskSpace: outOfDiskSpace, + FEEDBACK_TYPE_FEEDBACK: 'feedback', FEEDBACK_TYPE_TICKET: 'ticket', FEEDBACK_TYPE_APP: 'app', @@ -358,6 +360,19 @@ function appUpdateAvailable(app, updateInfo) { }); } +function outOfDiskSpace(message) { + assert.strictEqual(typeof message, 'string'); + + var mailOptions = { + from: config.get('adminEmail'), + to: 'admin@cloudron.io', + subject: util.format('[%s] Out of disk space alert', config.fqdn()), + text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' }) + }; + + sendMails([ mailOptions ]); +} + // this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized // crashnotifier should be able to send mail when there is no db function sendCrashNotification(program, context) {