Admin: Add message with failed / total nodes for nodes.json-task.
This commit is contained in:
parent
fb87695b3e
commit
b6a67d6e74
12 changed files with 120 additions and 20 deletions
|
@ -22,6 +22,14 @@
|
|||
color: red;
|
||||
}
|
||||
|
||||
.task-message.task-result-okay {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.task-message.task-result-warning {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
|
|
@ -383,7 +383,8 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Rest
|
|||
}
|
||||
return 'task-' + field + ' ' +
|
||||
(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('schedule').cssClasses(taskClasses('schedule')),
|
||||
nga.field('state').cssClasses(taskClasses('state')),
|
||||
nga.field('message').cssClasses(taskClasses('message')),
|
||||
nga.field('runningSince').map(formatMoment).cssClasses(taskClasses('runningSince')),
|
||||
nga.field('lastRunStarted').map(formatMoment).cssClasses(taskClasses('lastRunStarted')),
|
||||
nga.field('lastRunDuration').map(formatDuration).cssClasses(taskClasses('lastRunDuration'))
|
||||
|
|
|
@ -46,4 +46,3 @@ if confirm "Continue publishing?"; then
|
|||
cd dist
|
||||
npm publish
|
||||
fi
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import {fixNodeFilenames} from "../services/nodeService";
|
||||
import {jobResultOkay} from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'FixNodeFilenamesJob',
|
||||
description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.',
|
||||
|
||||
run: fixNodeFilenames
|
||||
async run() {
|
||||
await fixNodeFilenames();
|
||||
return jobResultOkay();
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import * as MailService from "../services/mailService"
|
||||
import {jobResultOkay} from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'MailQueueJob',
|
||||
description: 'Send pending emails (up to 5 attempts in case of failures).',
|
||||
|
||||
run: MailService.sendPendingMails,
|
||||
}
|
||||
async run() {
|
||||
await MailService.sendPendingMails();
|
||||
return jobResultOkay();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import * as MonitoringService from "../services/monitoringService";
|
||||
import {jobResultOkay} from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'MonitoringMailsSendingJob',
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
import * as MonitoringService from "../services/monitoringService";
|
||||
import {jobResultOkay, jobResultWarning} from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'NodeInformationRetrievalJob',
|
||||
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.`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import * as MonitoringService from "../services/monitoringService";
|
||||
import {jobResultOkay} from "./scheduler";
|
||||
|
||||
export default {
|
||||
name: 'OfflineNodesDeletionJob',
|
||||
description: 'Delete nodes that are offline for more than 100 days.',
|
||||
|
||||
run: MonitoringService.deleteOfflineNodes,
|
||||
async run() {
|
||||
await MonitoringService.deleteOfflineNodes();
|
||||
return jobResultOkay();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -10,11 +10,35 @@ import NodeInformationRetrievalJob from "./NodeInformationRetrievalJob";
|
|||
import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob";
|
||||
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 {
|
||||
name: string,
|
||||
description: string,
|
||||
|
||||
run(): Promise<void>,
|
||||
run(): Promise<JobResult>,
|
||||
}
|
||||
|
||||
export enum TaskState {
|
||||
|
@ -34,6 +58,7 @@ export class Task {
|
|||
public lastRunStarted: moment.Moment | null,
|
||||
public lastRunDuration: number | null,
|
||||
public state: TaskState,
|
||||
public result: JobResult | null,
|
||||
public enabled: boolean,
|
||||
) {}
|
||||
|
||||
|
@ -47,7 +72,7 @@ export class Task {
|
|||
this.lastRunStarted = this.runningSince;
|
||||
this.state = TaskState.RUNNING;
|
||||
|
||||
const done = (state: TaskState):void => {
|
||||
const done = (state: TaskState, result: JobResult | null): void => {
|
||||
const now = moment();
|
||||
const duration = now.diff(this.runningSince || now);
|
||||
Logger.tag('jobs').profile('[%sms]\t%s', duration, this.name);
|
||||
|
@ -55,13 +80,14 @@ export class Task {
|
|||
this.runningSince = null;
|
||||
this.lastRunDuration = duration;
|
||||
this.state = state;
|
||||
this.result = result;
|
||||
};
|
||||
|
||||
this.job.run().then(() => {
|
||||
done(TaskState.IDLE);
|
||||
this.job.run().then(result => {
|
||||
done(TaskState.IDLE, result);
|
||||
}).catch((err: any) => {
|
||||
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,
|
||||
TaskState.IDLE,
|
||||
true
|
||||
null,
|
||||
true,
|
||||
);
|
||||
|
||||
cron.schedule(expr, task.run);
|
||||
|
|
|
@ -3,11 +3,11 @@ import _ from "lodash";
|
|||
import CONSTRAINTS from "../validation/constraints";
|
||||
import ErrorTypes from "../utils/errorTypes";
|
||||
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 {forConstraint} from "../validation/validator";
|
||||
import {Request, Response} from "express";
|
||||
import {Entity} from "../utils/resources";
|
||||
|
||||
const isValidId = forConstraint(CONSTRAINTS.id, false);
|
||||
|
||||
|
@ -20,6 +20,8 @@ interface ExternalTask {
|
|||
lastRunStarted: number | null,
|
||||
lastRunDuration: number | null,
|
||||
state: string,
|
||||
result: string | null,
|
||||
message: string | null,
|
||||
enabled: boolean,
|
||||
}
|
||||
|
||||
|
@ -33,6 +35,8 @@ function toExternalTask(task: Task): ExternalTask {
|
|||
lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(),
|
||||
lastRunDuration: task.lastRunDuration || null,
|
||||
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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -389,6 +389,8 @@ test('parseNodesJson() should succeed parsing no nodes', () => {
|
|||
// then
|
||||
expect(result.importTimestamp.isValid()).toBe(true);
|
||||
expect(result.nodes).toEqual([]);
|
||||
expect(result.failedNodesCount).toEqual(0);
|
||||
expect(result.totalNodesCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('parseNodesJson() should skip parsing invalid nodes', () => {
|
||||
|
@ -423,6 +425,8 @@ test('parseNodesJson() should skip parsing invalid nodes', () => {
|
|||
// then
|
||||
expect(result.importTimestamp.isValid()).toBe(true);
|
||||
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);
|
||||
});
|
||||
|
||||
|
@ -467,5 +471,7 @@ test('parseNodesJson() should parse valid nodes', () => {
|
|||
|
||||
expect(result.importTimestamp.isValid()).toBe(true);
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -45,8 +45,15 @@ export type ParsedNode = {
|
|||
export type NodesParsingResult = {
|
||||
importTimestamp: Moment,
|
||||
nodes: ParsedNode[],
|
||||
failedNodesCount: number,
|
||||
totalNodesCount: number,
|
||||
}
|
||||
|
||||
export type RetrieveNodeInformationResult = {
|
||||
failedParsingNodesCount: number,
|
||||
totalNodesCount: number,
|
||||
};
|
||||
|
||||
let previousImportTimestamp: Moment | null = null;
|
||||
|
||||
async function insertNodeInformation(nodeData: ParsedNode, node: Node): Promise<void> {
|
||||
|
@ -228,7 +235,9 @@ export function parseNodesJson (body: string): NodesParsingResult {
|
|||
|
||||
const result: NodesParsingResult = {
|
||||
importTimestamp: parseTimestamp(json.timestamp),
|
||||
nodes: []
|
||||
nodes: [],
|
||||
failedNodesCount: 0,
|
||||
totalNodesCount: 0,
|
||||
};
|
||||
|
||||
if (!result.importTimestamp.isValid()) {
|
||||
|
@ -240,12 +249,14 @@ export function parseNodesJson (body: string): NodesParsingResult {
|
|||
}
|
||||
|
||||
for (const nodeData of json.nodes) {
|
||||
result.totalNodesCount += 1;
|
||||
try {
|
||||
const parsedNode = parseNode(result.importTimestamp, nodeData);
|
||||
Logger.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`);
|
||||
result.nodes.push(parsedNode);
|
||||
}
|
||||
catch (error) {
|
||||
result.failedNodesCount += 1;
|
||||
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;
|
||||
}
|
||||
|
||||
async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
|
||||
async function retrieveNodeInformationForUrls(urls: string[]): Promise<RetrieveNodeInformationResult> {
|
||||
const datas = await withUrlsData(urls);
|
||||
|
||||
let maxTimestamp = datas[0].importTimestamp;
|
||||
let minTimestamp = maxTimestamp;
|
||||
|
||||
let failedParsingNodesCount = 0;
|
||||
let totalNodesCount = 0;
|
||||
|
||||
for (const data of datas) {
|
||||
if (data.importTimestamp.isAfter(maxTimestamp)) {
|
||||
maxTimestamp = data.importTimestamp;
|
||||
|
@ -446,6 +461,9 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
|
|||
if (data.importTimestamp.isBefore(minTimestamp)) {
|
||||
minTimestamp = data.importTimestamp;
|
||||
}
|
||||
|
||||
failedParsingNodesCount += data.failedNodesCount;
|
||||
totalNodesCount += data.totalNodesCount;
|
||||
}
|
||||
|
||||
if (previousImportTimestamp !== null && !maxTimestamp.isAfter(previousImportTimestamp)) {
|
||||
|
@ -456,7 +474,10 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
|
|||
maxTimestamp.format(),
|
||||
previousImportTimestamp.format()
|
||||
);
|
||||
return;
|
||||
return {
|
||||
failedParsingNodesCount,
|
||||
totalNodesCount,
|
||||
};
|
||||
}
|
||||
previousImportTimestamp = maxTimestamp;
|
||||
|
||||
|
@ -502,6 +523,11 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
|
|||
minTimestamp.unix()
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
failedParsingNodesCount,
|
||||
totalNodesCount,
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export async function retrieveNodeInformation(): Promise<void> {
|
||||
export async function retrieveNodeInformation(): Promise<RetrieveNodeInformationResult> {
|
||||
const urls = config.server.map.nodesJsonUrl;
|
||||
if (_.isEmpty(urls)) {
|
||||
throw new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl')
|
||||
|
|
Loading…
Reference in a new issue