Admin: Add message with failed / total nodes for nodes.json-task.

This commit is contained in:
baldo 2020-06-30 17:08:24 +02:00
parent fb87695b3e
commit b6a67d6e74
12 changed files with 120 additions and 20 deletions

View file

@ -22,6 +22,14 @@
color: red; color: red;
} }
.task-message.task-result-okay {
color: green;
}
.task-message.task-result-warning {
color: red;
}
.task-description { .task-description {
max-width: 220px; max-width: 220px;
} }

View file

@ -383,7 +383,8 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Rest
} }
return 'task-' + field + ' ' + return 'task-' + field + ' ' +
(task.values.enabled ? 'task-enabled' : 'task-disabled') + ' ' (task.values.enabled ? 'task-enabled' : 'task-disabled') + ' '
+ 'task-state-' + task.values.state; + 'task-state-' + task.values.state + ' '
+ 'task-result-' + (task.values.result ? task.values.result : 'none');
}; };
} }
@ -403,6 +404,7 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Rest
nga.field('description').cssClasses(taskClasses('description')), nga.field('description').cssClasses(taskClasses('description')),
nga.field('schedule').cssClasses(taskClasses('schedule')), nga.field('schedule').cssClasses(taskClasses('schedule')),
nga.field('state').cssClasses(taskClasses('state')), nga.field('state').cssClasses(taskClasses('state')),
nga.field('message').cssClasses(taskClasses('message')),
nga.field('runningSince').map(formatMoment).cssClasses(taskClasses('runningSince')), nga.field('runningSince').map(formatMoment).cssClasses(taskClasses('runningSince')),
nga.field('lastRunStarted').map(formatMoment).cssClasses(taskClasses('lastRunStarted')), nga.field('lastRunStarted').map(formatMoment).cssClasses(taskClasses('lastRunStarted')),
nga.field('lastRunDuration').map(formatDuration).cssClasses(taskClasses('lastRunDuration')) nga.field('lastRunDuration').map(formatDuration).cssClasses(taskClasses('lastRunDuration'))

View file

@ -46,4 +46,3 @@ if confirm "Continue publishing?"; then
cd dist cd dist
npm publish npm publish
fi fi

View file

@ -1,8 +1,12 @@
import {fixNodeFilenames} from "../services/nodeService"; import {fixNodeFilenames} from "../services/nodeService";
import {jobResultOkay} from "./scheduler";
export default { export default {
name: 'FixNodeFilenamesJob', name: 'FixNodeFilenamesJob',
description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.', description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.',
run: fixNodeFilenames async run() {
await fixNodeFilenames();
return jobResultOkay();
},
} }

View file

@ -1,8 +1,12 @@
import * as MailService from "../services/mailService" import * as MailService from "../services/mailService"
import {jobResultOkay} from "./scheduler";
export default { export default {
name: 'MailQueueJob', name: 'MailQueueJob',
description: 'Send pending emails (up to 5 attempts in case of failures).', description: 'Send pending emails (up to 5 attempts in case of failures).',
run: MailService.sendPendingMails, async run() {
} await MailService.sendPendingMails();
return jobResultOkay();
},
};

View file

@ -1,8 +1,12 @@
import * as MonitoringService from "../services/monitoringService"; import * as MonitoringService from "../services/monitoringService";
import {jobResultOkay} from "./scheduler";
export default { export default {
name: 'MonitoringMailsSendingJob', name: 'MonitoringMailsSendingJob',
description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.', description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.',
run: MonitoringService.sendMonitoringMails, async run() {
await MonitoringService.sendMonitoringMails();
return jobResultOkay();
},
}; };

View file

@ -1,8 +1,20 @@
import * as MonitoringService from "../services/monitoringService"; import * as MonitoringService from "../services/monitoringService";
import {jobResultOkay, jobResultWarning} from "./scheduler";
export default { export default {
name: 'NodeInformationRetrievalJob', name: 'NodeInformationRetrievalJob',
description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.', description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.',
run: MonitoringService.retrieveNodeInformation, async run () {
const result = await MonitoringService.retrieveNodeInformation();
if (result.failedParsingNodesCount > 0) {
return jobResultWarning(
`Warning: ${result.failedParsingNodesCount} of ${result.totalNodesCount} nodes could not be processed.`
);
} else {
return jobResultOkay(
`${result.totalNodesCount} nodes have been processed.`
);
}
},
}; };

View file

@ -1,8 +1,12 @@
import * as MonitoringService from "../services/monitoringService"; import * as MonitoringService from "../services/monitoringService";
import {jobResultOkay} from "./scheduler";
export default { export default {
name: 'OfflineNodesDeletionJob', name: 'OfflineNodesDeletionJob',
description: 'Delete nodes that are offline for more than 100 days.', description: 'Delete nodes that are offline for more than 100 days.',
run: MonitoringService.deleteOfflineNodes, async run() {
await MonitoringService.deleteOfflineNodes();
return jobResultOkay();
},
}; };

View file

@ -10,11 +10,35 @@ import NodeInformationRetrievalJob from "./NodeInformationRetrievalJob";
import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob"; import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob";
import OfflineNodesDeletionJob from "./OfflineNodesDeletionJob"; import OfflineNodesDeletionJob from "./OfflineNodesDeletionJob";
export enum JobResultState {
OKAY = "okay",
WARNING = "warning",
}
export type JobResult = {
state: JobResultState,
message?: string,
};
export function jobResultOkay(message?: string): JobResult {
return {
state: JobResultState.OKAY,
message
}
}
export function jobResultWarning(message?: string): JobResult {
return {
state: JobResultState.WARNING,
message
}
}
export interface Job { export interface Job {
name: string, name: string,
description: string, description: string,
run(): Promise<void>, run(): Promise<JobResult>,
} }
export enum TaskState { export enum TaskState {
@ -34,6 +58,7 @@ export class Task {
public lastRunStarted: moment.Moment | null, public lastRunStarted: moment.Moment | null,
public lastRunDuration: number | null, public lastRunDuration: number | null,
public state: TaskState, public state: TaskState,
public result: JobResult | null,
public enabled: boolean, public enabled: boolean,
) {} ) {}
@ -47,7 +72,7 @@ export class Task {
this.lastRunStarted = this.runningSince; this.lastRunStarted = this.runningSince;
this.state = TaskState.RUNNING; this.state = TaskState.RUNNING;
const done = (state: TaskState):void => { const done = (state: TaskState, result: JobResult | null): void => {
const now = moment(); const now = moment();
const duration = now.diff(this.runningSince || now); const duration = now.diff(this.runningSince || now);
Logger.tag('jobs').profile('[%sms]\t%s', duration, this.name); Logger.tag('jobs').profile('[%sms]\t%s', duration, this.name);
@ -55,13 +80,14 @@ export class Task {
this.runningSince = null; this.runningSince = null;
this.lastRunDuration = duration; this.lastRunDuration = duration;
this.state = state; this.state = state;
this.result = result;
}; };
this.job.run().then(() => { this.job.run().then(result => {
done(TaskState.IDLE); done(TaskState.IDLE, result);
}).catch((err: any) => { }).catch((err: any) => {
Logger.tag('jobs').error("Job %s failed: %s", this.name, err); Logger.tag('jobs').error("Job %s failed: %s", this.name, err);
done(TaskState.FAILED); done(TaskState.FAILED, null);
}); });
} }
} }
@ -92,7 +118,8 @@ function schedule(expr: string, job: Job): void {
null, null,
null, null,
TaskState.IDLE, TaskState.IDLE,
true null,
true,
); );
cron.schedule(expr, task.run); cron.schedule(expr, task.run);

View file

@ -3,11 +3,11 @@ import _ from "lodash";
import CONSTRAINTS from "../validation/constraints"; import CONSTRAINTS from "../validation/constraints";
import ErrorTypes from "../utils/errorTypes"; import ErrorTypes from "../utils/errorTypes";
import * as Resources from "../utils/resources"; import * as Resources from "../utils/resources";
import {getTasks, Task} from "../jobs/scheduler"; import {Entity} from "../utils/resources";
import {getTasks, Task, TaskState} from "../jobs/scheduler";
import {normalizeString} from "../utils/strings"; import {normalizeString} from "../utils/strings";
import {forConstraint} from "../validation/validator"; import {forConstraint} from "../validation/validator";
import {Request, Response} from "express"; import {Request, Response} from "express";
import {Entity} from "../utils/resources";
const isValidId = forConstraint(CONSTRAINTS.id, false); const isValidId = forConstraint(CONSTRAINTS.id, false);
@ -20,6 +20,8 @@ interface ExternalTask {
lastRunStarted: number | null, lastRunStarted: number | null,
lastRunDuration: number | null, lastRunDuration: number | null,
state: string, state: string,
result: string | null,
message: string | null,
enabled: boolean, enabled: boolean,
} }
@ -33,6 +35,8 @@ function toExternalTask(task: Task): ExternalTask {
lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(), lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(),
lastRunDuration: task.lastRunDuration || null, lastRunDuration: task.lastRunDuration || null,
state: task.state, state: task.state,
result: task.state !== TaskState.RUNNING && task.result ? task.result.state : null,
message:task.state !== TaskState.RUNNING && task.result ? task.result.message || null : null,
enabled: task.enabled enabled: task.enabled
}; };
} }

View file

@ -389,6 +389,8 @@ test('parseNodesJson() should succeed parsing no nodes', () => {
// then // then
expect(result.importTimestamp.isValid()).toBe(true); expect(result.importTimestamp.isValid()).toBe(true);
expect(result.nodes).toEqual([]); expect(result.nodes).toEqual([]);
expect(result.failedNodesCount).toEqual(0);
expect(result.totalNodesCount).toEqual(0);
}); });
test('parseNodesJson() should skip parsing invalid nodes', () => { test('parseNodesJson() should skip parsing invalid nodes', () => {
@ -423,6 +425,8 @@ test('parseNodesJson() should skip parsing invalid nodes', () => {
// then // then
expect(result.importTimestamp.isValid()).toBe(true); expect(result.importTimestamp.isValid()).toBe(true);
expect(result.nodes).toEqual([]); expect(result.nodes).toEqual([]);
expect(result.failedNodesCount).toEqual(2);
expect(result.totalNodesCount).toEqual(2);
expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(2); expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(2);
}); });
@ -467,5 +471,7 @@ test('parseNodesJson() should parse valid nodes', () => {
expect(result.importTimestamp.isValid()).toBe(true); expect(result.importTimestamp.isValid()).toBe(true);
expect(result.nodes).toEqual([expectedParsedNode]); expect(result.nodes).toEqual([expectedParsedNode]);
expect(result.failedNodesCount).toEqual(1);
expect(result.totalNodesCount).toEqual(2);
expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(1); expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(1);
}); });

View file

@ -45,8 +45,15 @@ export type ParsedNode = {
export type NodesParsingResult = { export type NodesParsingResult = {
importTimestamp: Moment, importTimestamp: Moment,
nodes: ParsedNode[], nodes: ParsedNode[],
failedNodesCount: number,
totalNodesCount: number,
} }
export type RetrieveNodeInformationResult = {
failedParsingNodesCount: number,
totalNodesCount: number,
};
let previousImportTimestamp: Moment | null = null; let previousImportTimestamp: Moment | null = null;
async function insertNodeInformation(nodeData: ParsedNode, node: Node): Promise<void> { async function insertNodeInformation(nodeData: ParsedNode, node: Node): Promise<void> {
@ -228,7 +235,9 @@ export function parseNodesJson (body: string): NodesParsingResult {
const result: NodesParsingResult = { const result: NodesParsingResult = {
importTimestamp: parseTimestamp(json.timestamp), importTimestamp: parseTimestamp(json.timestamp),
nodes: [] nodes: [],
failedNodesCount: 0,
totalNodesCount: 0,
}; };
if (!result.importTimestamp.isValid()) { if (!result.importTimestamp.isValid()) {
@ -240,12 +249,14 @@ export function parseNodesJson (body: string): NodesParsingResult {
} }
for (const nodeData of json.nodes) { for (const nodeData of json.nodes) {
result.totalNodesCount += 1;
try { try {
const parsedNode = parseNode(result.importTimestamp, nodeData); const parsedNode = parseNode(result.importTimestamp, nodeData);
Logger.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`); Logger.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`);
result.nodes.push(parsedNode); result.nodes.push(parsedNode);
} }
catch (error) { catch (error) {
result.failedNodesCount += 1;
Logger.tag('monitoring', 'parsing-nodes-json').error("Could not parse node.", error, nodeData); Logger.tag('monitoring', 'parsing-nodes-json').error("Could not parse node.", error, nodeData);
} }
} }
@ -434,11 +445,15 @@ async function withUrlsData(urls: string[]): Promise<NodesParsingResult[]> {
return results; return results;
} }
async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> { async function retrieveNodeInformationForUrls(urls: string[]): Promise<RetrieveNodeInformationResult> {
const datas = await withUrlsData(urls); const datas = await withUrlsData(urls);
let maxTimestamp = datas[0].importTimestamp; let maxTimestamp = datas[0].importTimestamp;
let minTimestamp = maxTimestamp; let minTimestamp = maxTimestamp;
let failedParsingNodesCount = 0;
let totalNodesCount = 0;
for (const data of datas) { for (const data of datas) {
if (data.importTimestamp.isAfter(maxTimestamp)) { if (data.importTimestamp.isAfter(maxTimestamp)) {
maxTimestamp = data.importTimestamp; maxTimestamp = data.importTimestamp;
@ -446,6 +461,9 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
if (data.importTimestamp.isBefore(minTimestamp)) { if (data.importTimestamp.isBefore(minTimestamp)) {
minTimestamp = data.importTimestamp; minTimestamp = data.importTimestamp;
} }
failedParsingNodesCount += data.failedNodesCount;
totalNodesCount += data.totalNodesCount;
} }
if (previousImportTimestamp !== null && !maxTimestamp.isAfter(previousImportTimestamp)) { if (previousImportTimestamp !== null && !maxTimestamp.isAfter(previousImportTimestamp)) {
@ -456,7 +474,10 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
maxTimestamp.format(), maxTimestamp.format(),
previousImportTimestamp.format() previousImportTimestamp.format()
); );
return; return {
failedParsingNodesCount,
totalNodesCount,
};
} }
previousImportTimestamp = maxTimestamp; previousImportTimestamp = maxTimestamp;
@ -502,6 +523,11 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
minTimestamp.unix() minTimestamp.unix()
] ]
); );
return {
failedParsingNodesCount,
totalNodesCount,
}
} }
export async function getAll(restParams: RestParams): Promise<{total: number, monitoringStates: any[]}> { export async function getAll(restParams: RestParams): Promise<{total: number, monitoringStates: any[]}> {
@ -605,7 +631,7 @@ export async function disable(token: string): Promise<Node> {
return newNode; return newNode;
} }
export async function retrieveNodeInformation(): Promise<void> { export async function retrieveNodeInformation(): Promise<RetrieveNodeInformationResult> {
const urls = config.server.map.nodesJsonUrl; const urls = config.server.map.nodesJsonUrl;
if (_.isEmpty(urls)) { if (_.isEmpty(urls)) {
throw new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl') throw new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl')