Admin: Add message with failed / total nodes for nodes.json-task.
This commit is contained in:
parent
fb87695b3e
commit
b6a67d6e74
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
|
@ -46,4 +46,3 @@ if confirm "Continue publishing?"; then
|
||||||
cd dist
|
cd dist
|
||||||
npm publish
|
npm publish
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue