ef476f74bf
emails will not be used for self monitoring events. these are best done from the outside. we just log everything in eventlog and raise notifications as well.
391 lines
15 KiB
JavaScript
391 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
get,
|
|
ack,
|
|
getAllPaged,
|
|
|
|
onEvent,
|
|
|
|
appUpdatesAvailable,
|
|
boxUpdateAvailable,
|
|
|
|
// NOTE: if you add an alert, be sure to add title below
|
|
ALERT_BACKUP_CONFIG: 'backupConfig',
|
|
ALERT_DISK_SPACE: 'diskSpace',
|
|
ALERT_MAIL_STATUS: 'mailStatus',
|
|
ALERT_REBOOT: 'reboot',
|
|
ALERT_BOX_UPDATE: 'boxUpdate',
|
|
|
|
alert: alert,
|
|
|
|
// exported for testing
|
|
_add: add
|
|
};
|
|
|
|
let apps = require('./apps.js'),
|
|
assert = require('assert'),
|
|
async = require('async'),
|
|
auditSource = require('./auditsource.js'),
|
|
BoxError = require('./boxerror.js'),
|
|
changelog = require('./changelog.js'),
|
|
constants = require('./constants.js'),
|
|
debug = require('debug')('box:notifications'),
|
|
eventlog = require('./eventlog.js'),
|
|
mailer = require('./mailer.js'),
|
|
notificationdb = require('./notificationdb.js'),
|
|
settings = require('./settings.js'),
|
|
users = require('./users.js');
|
|
|
|
function add(userId, eventId, title, message, callback) {
|
|
assert.strictEqual(typeof userId, 'string');
|
|
assert(typeof eventId === 'string' || eventId === null);
|
|
assert.strictEqual(typeof title, 'string');
|
|
assert.strictEqual(typeof message, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
debug('add: ', userId, title);
|
|
|
|
notificationdb.add({
|
|
userId: userId,
|
|
eventId: eventId,
|
|
title: title,
|
|
message: message,
|
|
acknowledged: false
|
|
}, function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
callback(null, { id: result });
|
|
});
|
|
}
|
|
|
|
function get(id, callback) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
notificationdb.get(id, function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
callback(null, result);
|
|
});
|
|
}
|
|
|
|
function ack(id, callback) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
notificationdb.update(id, { acknowledged: true }, function (error) {
|
|
if (error) return callback(error);
|
|
|
|
callback(null);
|
|
});
|
|
}
|
|
|
|
// if acknowledged === null we return all, otherwise yes or no based on acknowledged as a boolean
|
|
function getAllPaged(userId, acknowledged, page, perPage, callback) {
|
|
assert.strictEqual(typeof userId, 'string');
|
|
assert(acknowledged === null || typeof acknowledged === 'boolean');
|
|
assert.strictEqual(typeof page, 'number');
|
|
assert.strictEqual(typeof perPage, 'number');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
notificationdb.listByUserIdPaged(userId, page, perPage, function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
if (acknowledged === null) return callback(null, result);
|
|
|
|
callback(null, result.filter(function (r) { return r.acknowledged === acknowledged; }));
|
|
});
|
|
}
|
|
|
|
// Calls iterator with (admin, callback)
|
|
function forEachAdmin(options, iterator, callback) {
|
|
assert(Array.isArray(options.skip));
|
|
assert.strictEqual(typeof iterator, 'function');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
users.getAdmins(function (error, result) {
|
|
if (error && error.reason === BoxError.NOT_FOUND) return callback();
|
|
if (error) return callback(error);
|
|
|
|
// filter out users we want to skip (like the user who did the action or the user the action was performed on)
|
|
result = result.filter(function (r) { return options.skip.indexOf(r.id) === -1; });
|
|
|
|
async.each(result, iterator, callback);
|
|
});
|
|
}
|
|
|
|
function userAdded(performedBy, eventId, user, callback) {
|
|
assert.strictEqual(typeof performedBy, 'string');
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof user, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
forEachAdmin({ skip: [ performedBy, user.id ] }, function (admin, done) {
|
|
add(admin.id, eventId, `User '${user.displayName}' added`, `User '${user.username || user.email || user.fallbackEmail}' was added.`, done);
|
|
}, callback);
|
|
}
|
|
|
|
function userRemoved(performedBy, eventId, user, callback) {
|
|
assert.strictEqual(typeof performedBy, 'string');
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof user, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
forEachAdmin({ skip: [ performedBy, user.id ] }, function (admin, done) {
|
|
add(admin.id, eventId, `User '${user.displayName}' removed`, `User '${user.username || user.email || user.fallbackEmail}' was removed.`, done);
|
|
}, callback);
|
|
}
|
|
|
|
function roleChanged(performedBy, eventId, user, callback) {
|
|
assert.strictEqual(typeof performedBy, 'string');
|
|
assert.strictEqual(typeof user, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
forEachAdmin({ skip: [ performedBy, user.id ] }, function (admin, done) {
|
|
add(admin.id, eventId, `User '${user.displayName}'s role changed`, `User '${user.username || user.email || user.fallbackEmail}' now has the role ${user.role}.`, done);
|
|
}, callback);
|
|
}
|
|
|
|
function oomEvent(eventId, app, addon, containerId, event, callback) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof addon, 'object');
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
assert(app || addon);
|
|
|
|
let title, message;
|
|
if (app) {
|
|
title = `The application at ${app.fqdn} ran out of memory.`;
|
|
message = `The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](${settings.adminOrigin()}/#/app/${app.id}/resources)`;
|
|
} else if (addon) {
|
|
title = `The ${addon.name} service ran out of memory`;
|
|
message = `The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](${settings.adminOrigin()}/#/services)`;
|
|
}
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, done) {
|
|
add(admin.id, eventId, title, message, done);
|
|
}, callback);
|
|
}
|
|
|
|
function appUp(eventId, app, callback) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, done) {
|
|
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application installed at ${app.fqdn} is back online.`, done);
|
|
}, callback);
|
|
}
|
|
|
|
function appDied(eventId, app, callback) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, callback) {
|
|
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application installed at ${app.fqdn} is not responding.`, callback);
|
|
}, callback);
|
|
}
|
|
|
|
function appUpdated(eventId, app, callback) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
if (!app.appStoreId) return callback(); // skip notification of dev apps
|
|
|
|
const tmp = app.manifest.description.match(/<upstream>(.*)<\/upstream>/i);
|
|
const upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
|
|
const title = upstreamVersion ? `${app.manifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${app.manifest.version})`
|
|
: `${app.manifest.title} at ${app.fqdn} updated to package version ${app.manifest.version}`;
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, done) {
|
|
add(admin.id, eventId, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
|
|
if (error) return callback(error);
|
|
|
|
mailer.appUpdated(admin.email, app, function (error) {
|
|
if (error) debug('appUpdated: Failed to send app updated email', error); // non fatal
|
|
done();
|
|
});
|
|
});
|
|
}, callback);
|
|
}
|
|
|
|
function boxUpdateAvailable(updateInfo, callback) {
|
|
assert.strictEqual(typeof updateInfo, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
settings.getAutoupdatePattern(function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
if (result !== constants.AUTOUPDATE_PATTERN_NEVER) return callback();
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, done) {
|
|
mailer.boxUpdateAvailable(admin.email, updateInfo, done);
|
|
}, callback);
|
|
});
|
|
}
|
|
|
|
function appUpdatesAvailable(appUpdates, callback) {
|
|
assert.strictEqual(typeof appUpdates, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
settings.getAutoupdatePattern(function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
// if we are auto updating, then just consider apps that cannot be auto updated
|
|
if (result !== constants.AUTOUPDATE_PATTERN_NEVER) appUpdates = appUpdates.filter(update => !apps.canAutoupdateApp(update.app, update.updateInfo));
|
|
|
|
if (appUpdates.length === 0) return callback();
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, done) {
|
|
mailer.appUpdatesAvailable(admin.email, appUpdates, done);
|
|
}, callback);
|
|
});
|
|
}
|
|
|
|
function boxUpdated(eventId, oldVersion, newVersion, callback) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof oldVersion, 'string');
|
|
assert.strictEqual(typeof newVersion, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
const changes = changelog.getChanges(newVersion);
|
|
const changelogMarkdown = changes.map((m) => `* ${m}\n`).join('');
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, done) {
|
|
add(admin.id, eventId, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, done);
|
|
}, callback);
|
|
}
|
|
|
|
function boxUpdateError(eventId, errorMessage, callback) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof errorMessage, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, done) {
|
|
mailer.boxUpdateError(admin.email, errorMessage);
|
|
add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`, done);
|
|
}, callback);
|
|
}
|
|
|
|
function certificateRenewalError(eventId, vhost, errorMessage, callback) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof vhost, 'string');
|
|
assert.strictEqual(typeof errorMessage, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, callback) {
|
|
mailer.certificateRenewalError(admin.email, vhost, errorMessage);
|
|
add(admin.id, eventId, `Certificate renewal of ${vhost} failed`, `Failed to renew certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours`, callback);
|
|
}, callback);
|
|
}
|
|
|
|
function backupFailed(eventId, taskId, errorMessage, callback) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof taskId, 'string');
|
|
assert.strictEqual(typeof errorMessage, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, callback) {
|
|
mailer.backupFailed(admin.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
|
|
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`, callback);
|
|
}, callback);
|
|
}
|
|
|
|
function alert(id, title, message, callback) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
assert.strictEqual(typeof title, 'string');
|
|
assert.strictEqual(typeof message, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
const acknowledged = !message;
|
|
debug(`alert: id=${id} title=${title} ack=${acknowledged}`);
|
|
|
|
forEachAdmin({ skip: [] }, function (admin, callback) {
|
|
const data = {
|
|
userId: admin.id,
|
|
eventId: null,
|
|
title: title,
|
|
message: message,
|
|
acknowledged: acknowledged,
|
|
creationTime: new Date()
|
|
};
|
|
|
|
notificationdb.getByUserIdAndTitle(admin.id, title, function (error, result) {
|
|
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
|
|
|
|
if (!result && acknowledged) return callback(); // do not add acked alerts
|
|
|
|
let updateFunc = !result ? notificationdb.add.bind(null, data) : notificationdb.update.bind(null, result.id, data);
|
|
|
|
updateFunc(function (error) {
|
|
if (error) return callback(error);
|
|
|
|
callback(null);
|
|
});
|
|
});
|
|
}, function (error) {
|
|
if (error) debug('alert: error notifying', error);
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function onEvent(id, action, source, data, callback) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
assert.strictEqual(typeof action, 'string');
|
|
assert.strictEqual(typeof source, 'object');
|
|
assert.strictEqual(typeof data, 'object');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
// external ldap syncer does not generate notifications - FIXME username might be an issue here
|
|
if (source.username === auditSource.EXTERNAL_LDAP_TASK.username) return callback();
|
|
|
|
switch (action) {
|
|
case eventlog.ACTION_USER_ADD:
|
|
return userAdded(source.userId, id, data.user, callback);
|
|
|
|
case eventlog.ACTION_USER_REMOVE:
|
|
return userRemoved(source.userId, id, data.user, callback);
|
|
|
|
case eventlog.ACTION_USER_UPDATE:
|
|
if (!data.roleChanged) return callback();
|
|
return roleChanged(source.userId, id, data.user, callback);
|
|
|
|
case eventlog.ACTION_APP_OOM:
|
|
return oomEvent(id, data.app, data.addon, data.containerId, data.event, callback);
|
|
|
|
case eventlog.ACTION_APP_DOWN:
|
|
return appDied(id, data.app, callback);
|
|
|
|
case eventlog.ACTION_APP_UP:
|
|
return appUp(id, data.app, callback);
|
|
|
|
case eventlog.ACTION_APP_UPDATE_FINISH:
|
|
return appUpdated(id, data.app, callback);
|
|
|
|
case eventlog.ACTION_CERTIFICATE_RENEWAL:
|
|
case eventlog.ACTION_CERTIFICATE_NEW:
|
|
if (!data.errorMessage) return callback();
|
|
return certificateRenewalError(id, data.domain, data.errorMessage, callback);
|
|
|
|
case eventlog.ACTION_BACKUP_FINISH:
|
|
if (!data.errorMessage) return callback();
|
|
if (source.username !== auditSource.CRON.username && !data.timedOut) return callback(); // manual stop by user
|
|
|
|
return backupFailed(id, data.taskId, data.errorMessage, callback); // only notify for automated backups or timedout
|
|
|
|
case eventlog.ACTION_UPDATE_FINISH:
|
|
if (!data.errorMessage) return boxUpdated(id, data.oldVersion, data.newVersion, callback);
|
|
if (data.timedOut) return boxUpdateError(id, data.errorMessage, callback);
|
|
return callback();
|
|
|
|
default:
|
|
return callback();
|
|
}
|
|
}
|