268 lines
9.3 KiB
JavaScript
268 lines
9.3 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
get,
|
|
update,
|
|
list,
|
|
del,
|
|
|
|
onEvent,
|
|
|
|
ALERT_BACKUP_CONFIG: 'backupConfig',
|
|
ALERT_DISK_SPACE: 'diskSpace',
|
|
ALERT_MAIL_STATUS: 'mailStatus',
|
|
ALERT_REBOOT: 'reboot',
|
|
ALERT_BOX_UPDATE: 'boxUpdate',
|
|
ALERT_UPDATE_UBUNTU: 'ubuntuUpdate',
|
|
|
|
alert,
|
|
|
|
// exported for testing
|
|
_add: add
|
|
};
|
|
|
|
const assert = require('assert'),
|
|
auditSource = require('./auditsource.js'),
|
|
BoxError = require('./boxerror.js'),
|
|
changelog = require('./changelog.js'),
|
|
database = require('./database.js'),
|
|
eventlog = require('./eventlog.js'),
|
|
mailer = require('./mailer.js'),
|
|
settings = require('./settings.js'),
|
|
users = require('./users.js'),
|
|
util = require('util');
|
|
|
|
const NOTIFICATION_FIELDS = [ 'id', 'eventId', 'title', 'message', 'creationTime', 'acknowledged' ];
|
|
|
|
function postProcess(result) {
|
|
assert.strictEqual(typeof result, 'object');
|
|
result.id = String(result.id);
|
|
|
|
// convert to boolean
|
|
result.acknowledged = !!result.acknowledged;
|
|
return result;
|
|
}
|
|
|
|
async function add(eventId, title, message) {
|
|
assert(typeof eventId === 'string' || eventId === null);
|
|
assert.strictEqual(typeof title, 'string');
|
|
assert.strictEqual(typeof message, 'string');
|
|
|
|
const query = 'INSERT INTO notifications (eventId, title, message, acknowledged) VALUES (?, ?, ?, ?)';
|
|
const args = [ eventId, title, message, false ];
|
|
|
|
const result = await database.query(query, args);
|
|
return String(result.insertId);
|
|
}
|
|
|
|
async function get(id) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
|
|
const result = await database.query('SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE id = ?', [ id ]);
|
|
if (result.length === 0) return null;
|
|
|
|
return postProcess(result[0]);
|
|
}
|
|
|
|
async function getByTitle(title) {
|
|
assert.strictEqual(typeof title, 'string');
|
|
|
|
const results = await database.query('SELECT ' + NOTIFICATION_FIELDS + ' from notifications WHERE title = ? ORDER BY creationTime LIMIT 1', [ title ]);
|
|
if (results.length === 0) return null;
|
|
|
|
return postProcess(results[0]);
|
|
}
|
|
|
|
async function update(notification, data) {
|
|
assert.strictEqual(typeof notification, 'object');
|
|
assert.strictEqual(typeof data, 'object');
|
|
|
|
let args = [ ];
|
|
let fields = [ ];
|
|
for (let k in data) {
|
|
fields.push(k + ' = ?');
|
|
args.push(data[k]);
|
|
}
|
|
args.push(notification.id);
|
|
|
|
const result = await database.query('UPDATE notifications SET ' + fields.join(', ') + ' WHERE id = ?', args);
|
|
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Notification not found');
|
|
}
|
|
|
|
async function del(id) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
|
|
const result = await database.query('DELETE FROM notifications WHERE id = ?', [ id ]);
|
|
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Notification not found');
|
|
}
|
|
|
|
async function list(filters, page, perPage) {
|
|
assert.strictEqual(typeof filters, 'object');
|
|
assert.strictEqual(typeof page, 'number');
|
|
assert.strictEqual(typeof perPage, 'number');
|
|
|
|
let args = [];
|
|
|
|
let where = [];
|
|
if ('acknowledged' in filters) {
|
|
where.push('acknowledged=?');
|
|
args.push(filters.acknowledged);
|
|
}
|
|
|
|
let query = `SELECT ${NOTIFICATION_FIELDS} FROM notifications`;
|
|
if (where.length) query += ' WHERE ' + where.join(' AND ');
|
|
query += ' ORDER BY creationTime DESC LIMIT ?,?';
|
|
|
|
args.push((page-1)*perPage);
|
|
args.push(perPage);
|
|
|
|
const results = await database.query(query, args);
|
|
results.forEach(postProcess);
|
|
return results;
|
|
}
|
|
|
|
async function oomEvent(eventId, app, addon, containerId /*, event*/) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof addon, 'object');
|
|
assert.strictEqual(typeof containerId, 'string');
|
|
|
|
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.dashboardOrigin()}/#/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.dashboardOrigin()}/#/services)`;
|
|
}
|
|
|
|
await add(eventId, title, message);
|
|
}
|
|
|
|
async function appUpdated(eventId, app) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof app, 'object');
|
|
|
|
if (!app.appStoreId) return; // 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}`;
|
|
|
|
await add(eventId, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`);
|
|
}
|
|
|
|
async function boxUpdated(eventId, oldVersion, newVersion) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof oldVersion, 'string');
|
|
assert.strictEqual(typeof newVersion, 'string');
|
|
|
|
const changes = changelog.getChanges(newVersion);
|
|
const changelogMarkdown = changes.map((m) => `* ${m}\n`).join('');
|
|
|
|
await add(eventId, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`);
|
|
}
|
|
|
|
async function boxUpdateError(eventId, errorMessage) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof errorMessage, 'string');
|
|
|
|
await add(eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`);
|
|
}
|
|
|
|
async function certificateRenewalError(eventId, vhost, errorMessage) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof vhost, 'string');
|
|
assert.strictEqual(typeof errorMessage, 'string');
|
|
|
|
const getAdmins = util.callbackify(users.getAdmins);
|
|
|
|
const admins = await getAdmins();
|
|
|
|
for (const admin of admins) {
|
|
mailer.certificateRenewalError(admin.email, vhost, errorMessage);
|
|
}
|
|
|
|
await add(eventId, `Certificate renewal of ${vhost} failed`, `Failed to renew certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours`);
|
|
}
|
|
|
|
async function backupFailed(eventId, taskId, errorMessage) {
|
|
assert.strictEqual(typeof eventId, 'string');
|
|
assert.strictEqual(typeof taskId, 'string');
|
|
assert.strictEqual(typeof errorMessage, 'string');
|
|
|
|
const getSuperAdmins = util.callbackify(users.getSuperAdmins);
|
|
|
|
const superAdmins = await getSuperAdmins();
|
|
|
|
for (const superAdmin of superAdmins) {
|
|
mailer.backupFailed(superAdmin.email, errorMessage, `${settings.dashboardOrigin()}/logs.html?taskId=${taskId}`);
|
|
}
|
|
|
|
await add(eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`);
|
|
}
|
|
|
|
async function alert(id, title, message) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
assert.strictEqual(typeof title, 'string');
|
|
assert.strictEqual(typeof message, 'string');
|
|
|
|
const acknowledged = !message;
|
|
|
|
const result = await getByTitle(title);
|
|
if (!result && acknowledged) return; // do not add acked alerts
|
|
|
|
if (!result) {
|
|
await add(null /* eventId */, title, message);
|
|
} else {
|
|
await update(result, {
|
|
eventId: null,
|
|
title,
|
|
message,
|
|
acknowledged,
|
|
creationTime: new Date()
|
|
});
|
|
}
|
|
}
|
|
|
|
async function onEvent(id, action, source, data) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
assert.strictEqual(typeof action, 'string');
|
|
assert.strictEqual(typeof source, 'object');
|
|
assert.strictEqual(typeof data, 'object');
|
|
|
|
// external ldap syncer does not generate notifications - FIXME username might be an issue here
|
|
if (source.username === auditSource.EXTERNAL_LDAP_TASK.username) return;
|
|
|
|
switch (action) {
|
|
case eventlog.ACTION_APP_OOM:
|
|
return await oomEvent(id, data.app, data.addon, data.containerId, data.event);
|
|
|
|
case eventlog.ACTION_APP_UPDATE_FINISH:
|
|
return await appUpdated(id, data.app);
|
|
|
|
case eventlog.ACTION_CERTIFICATE_RENEWAL:
|
|
case eventlog.ACTION_CERTIFICATE_NEW:
|
|
if (!data.errorMessage) return;
|
|
if (!data.notAfter || (data.notAfter - new Date() >= (60 * 60 * 24 * 10 * 1000))) return; // more than 10 days left to expire
|
|
return await certificateRenewalError(id, data.domain, data.errorMessage);
|
|
|
|
case eventlog.ACTION_BACKUP_FINISH:
|
|
if (!data.errorMessage) return;
|
|
if (source.username !== auditSource.CRON.username && !data.timedOut) return; // manual stop by user
|
|
|
|
return await backupFailed(id, data.taskId, data.errorMessage); // only notify for automated backups or timedout
|
|
|
|
case eventlog.ACTION_UPDATE_FINISH:
|
|
if (!data.errorMessage) return await boxUpdated(id, data.oldVersion, data.newVersion);
|
|
if (data.timedOut) return await boxUpdateError(id, data.errorMessage);
|
|
return;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
}
|