diff --git a/src/API.ts b/src/API.ts index 9411b8e1f..8a1ca4578 100644 --- a/src/API.ts +++ b/src/API.ts @@ -19,6 +19,7 @@ import {TicketerConfig} from './api/TicketerConfig'; import {AlarmDAO} from './dao/AlarmDAO'; import {EventDAO} from './dao/EventDAO'; import {NodeDAO} from './dao/NodeDAO'; +import {SituationFeedbackDAO} from './dao/SituationFeedbackDAO'; import {V1FilterProcessor} from './dao/V1FilterProcessor'; import {V2FilterProcessor} from './dao/V2FilterProcessor'; @@ -40,6 +41,8 @@ import {OnmsPrimaryType, PrimaryTypes} from './model/OnmsPrimaryType'; import {OnmsServiceStatusType, ServiceStatusTypes} from './model/OnmsServiceStatusType'; import {OnmsServiceType, ServiceTypes} from './model/OnmsServiceType'; import {OnmsSeverity, Severities} from './model/OnmsSeverity'; +import {OnmsSituationFeedback} from './model/OnmsSituationFeedback'; +import {OnmsSituationFeedbackType} from './model/OnmsSituationFeedbackType'; import {OnmsSnmpInterface} from './model/OnmsSnmpInterface'; import {OnmsSnmpStatusType, SnmpStatusTypes} from './model/OnmsSnmpStatusType'; import {OnmsTroubleTicketState, TroubleTicketStates} from './model/OnmsTroubleTicketState'; @@ -87,6 +90,7 @@ const DAO = Object.freeze({ AlarmDAO, EventDAO, NodeDAO, + SituationFeedbackDAO, V1FilterProcessor, V2FilterProcessor, }); @@ -120,6 +124,8 @@ const Model = Object.freeze({ ServiceTypes, OnmsSeverity, Severities, + OnmsSituationFeedback, + OnmsSituationFeedbackType, OnmsSnmpInterface, OnmsSnmpStatusType, SnmpStatusTypes, diff --git a/src/Client.ts b/src/Client.ts index 8d2b978ce..f2c4b899b 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -21,6 +21,7 @@ import {AlarmDAO} from './dao/AlarmDAO'; import {EventDAO} from './dao/EventDAO'; import {FlowDAO} from './dao/FlowDAO'; import {NodeDAO} from './dao/NodeDAO'; +import {SituationFeedbackDAO} from './dao/SituationFeedbackDAO'; import {AxiosHTTP} from './rest/AxiosHTTP'; @@ -165,4 +166,9 @@ export class Client implements IHasHTTP { public flows() { return new FlowDAO(this); } + + /** Get a situationFeedback DAO for submitting and querying correlation feedback. */ + public situationfeedback() { + return new SituationFeedbackDAO(this); + } } diff --git a/src/api/OnmsServer.ts b/src/api/OnmsServer.ts index c1f786a47..0a8cb6585 100644 --- a/src/api/OnmsServer.ts +++ b/src/api/OnmsServer.ts @@ -70,10 +70,12 @@ export class OnmsServer { if (forFragment === undefined) { return this.url; } + let uri = URI(this.url); if (forFragment.indexOf('/') === 0 || forFragment.indexOf('http') === 0) { - return forFragment; + uri = URI(forFragment); + } else { + uri = uri.segment(forFragment); } - let uri = URI(this.url).segment(forFragment); if (withQuery !== undefined) { uri = uri.addQuery(withQuery); } diff --git a/src/dao/SituationFeedbackDAO.ts b/src/dao/SituationFeedbackDAO.ts new file mode 100644 index 000000000..941b3f6ed --- /dev/null +++ b/src/dao/SituationFeedbackDAO.ts @@ -0,0 +1,121 @@ +import {BaseDAO} from './BaseDAO'; + +import {IHasHTTP} from '../api/IHasHTTP'; +import {IHash} from '../internal/IHash'; +import {IOnmsHTTP} from '../api/IOnmsHTTP'; +import {OnmsError} from '../api/OnmsError'; +import {OnmsHTTPOptions} from '../api/OnmsHTTPOptions'; + +import {OnmsSituationFeedback} from '../model/OnmsSituationFeedback'; +import {FeedbackTypes, OnmsSituationFeedbackType} from '../model/OnmsSituationFeedbackType'; + +import {OnmsParm} from '../model/OnmsParm'; +import {OnmsServiceType} from '../model/OnmsServiceType'; + +import {log, catDao} from '../api/Log'; +import {Category} from 'typescript-logging'; + +/** + * Data access for [[OnmsSituationFeedback]] objects. + * @module SituationFeedbackDAO + */ +export class SituationFeedbackDAO extends BaseDAO { + + constructor(impl: IHasHTTP | IOnmsHTTP) { + super(impl); + } + + /** + * Retrieve feedback. + * + * @version ReST v1 + * @param {number} situationId - The alarmId of the Situation to use when querying. + * @return An array of [[OnmsSituationFeedback]] objects. + */ + public async getFeedback(situationId: number): Promise { + const options = new OnmsHTTPOptions(); + options.headers.accept = 'application/json'; + return this.http.get(this.pathToEndpoint() + '/' + situationId, options).then((result) => { + const data = this.getData(result); + if (!Array.isArray(data)) { + if (!data) { + return [] as OnmsSituationFeedback[]; + } + throw new OnmsError('Expected an array of feedback but got "' + (typeof data) + '" instead.'); + } + return data.map((feedbackData) => { + return this.fromData(feedbackData); + }); + }); + } + + /** + * Submit Correlation Feedback for a Situation. + * + * @version ReST v1 + * @param {number} situationId - The alarmId of the Situation to use when querying. + * @param {OnmsSituationFeedback[]} feedback - The [[OnmsSituationFeedback]]. + */ + public async saveFeedback(feedback: OnmsSituationFeedback[], situationId: number): Promise { + return this.post(this.pathToEndpoint() + '/' + situationId, feedback); + } + + /** + * Extracts the data from an HTTP Request result. + * + * @param result the HTTP Request result. + * @returns An array of [[OnmsSituationFeedback]] objects. + */ + public getData(result: any): OnmsSituationFeedback[] { + const data = result.data; + if (!Array.isArray(data)) { + throw new OnmsError('Expected an array of situationFeedback but got "' + (typeof data) + '" instead.'); + } + return data; + } + + /** + * Generate a feedback object from the given dictionary. + * @hidden + */ + public fromData(data: any) { + const feedback = new OnmsSituationFeedback(); + feedback.situationKey = data.situationKey; + feedback.fingerprint = data.situationFingerprint; + feedback.alarmKey = data.alarmKey; + feedback.reason = data.reason; + feedback.user = data.user; + if (data.feedbackType) { + const fbt = data.feedbackType; + feedback.feedbackType = OnmsSituationFeedbackType.forId(fbt); + } + feedback.timestamp = this.toNumber(data.timestamp); + return feedback; + } + + /** + * Call a POST request in the format the SituationFeedback API expects. + * @hidden + */ + private async post(url: string, data: any): Promise { + const options = new OnmsHTTPOptions(); + options.headers['content-type'] = 'application/json'; + options.headers.accept = 'application/json'; + options.data = data; + return this.http.post(url, options).then((result) => { + if (!result.isSuccess) { + throw result; + } + return; + }); + } + + /** + * Get the path to the SituationFeedback endpoint. + * @hidden + */ + private pathToEndpoint() { + return 'rest/situation-feedback'; + } + +} diff --git a/src/model/OnmsSituationFeedback.ts b/src/model/OnmsSituationFeedback.ts new file mode 100644 index 000000000..0bc4f1f07 --- /dev/null +++ b/src/model/OnmsSituationFeedback.ts @@ -0,0 +1,34 @@ +import {IHasUrlValue} from '../api/IHasUrlValue'; +import {OnmsSituationFeedbackType} from './OnmsSituationFeedbackType'; + +/** + * Represents an OpenNMS alarm. + * @module OnmsAlarm + */ +export class OnmsSituationFeedback implements IHasUrlValue { + + /** the situation reduction key */ + public situationKey: string; + + /** signature of situation having given set of alarms */ + public fingerprint: string; + + /** the related alarm reduction key */ + public alarmKey: string; + + /** the related alarm reduction key */ + public feedbackType: OnmsSituationFeedbackType; + + /** the related alarm reduction key */ + public reason: string; + + /** the related alarm reduction key */ + public user: string; + + /** the related alarm reduction key */ + public timestamp: number; + + public get urlValue() { + return String(this.situationKey); + } +} diff --git a/src/model/OnmsSituationFeedbackType.ts b/src/model/OnmsSituationFeedbackType.ts new file mode 100644 index 000000000..8661603cf --- /dev/null +++ b/src/model/OnmsSituationFeedbackType.ts @@ -0,0 +1,37 @@ +import {IHasUrlValue} from '../api/IHasUrlValue'; + +import {OnmsEnum, forId, forLabel} from '../internal/OnmsEnum'; + +/** + * Represents an OpenNMS "SituationFeedback" type. + * @module OnmsSituationFeedbackType + */ +export class OnmsSituationFeedbackType extends OnmsEnum implements IHasUrlValue { + /** Given an ID, return the matching SituationFeedback type object. */ + public static forId(id: string) { + return forId(FeedbackTypes, id); + } + + /** Given a label, return the matching snmp status type object. */ + public static forLabel(label: string) { + return forLabel(FeedbackTypes, label); + } + + public get urlValue() { + return String(this.id); + } +} + +/* tslint:disable:object-literal-sort-keys */ +const FeedbackTypes = { + /** Alarm is correctly correlated */ + CORRECT: new OnmsSituationFeedbackType('CORRECT', 'CORRECT'), + /** Alarm was incorrectly correlated */ + FALSE_POSITIVE: new OnmsSituationFeedbackType('FALSE_POSITIVE', 'FALSE_POSITIVE'), + /** Alarm was incorrectly ommitted */ + FALSE_NEGATIVE: new OnmsSituationFeedbackType('FALSE_NEGATIVE', 'FALSE_NEGATIVE'), +}; + +/** @hidden */ +const frozen = Object.freeze(FeedbackTypes); +export {frozen as FeedbackTypes}; diff --git a/test/api/OnmsServer.spec.ts b/test/api/OnmsServer.spec.ts index 36843f3c6..0483e2bd8 100644 --- a/test/api/OnmsServer.spec.ts +++ b/test/api/OnmsServer.spec.ts @@ -51,6 +51,21 @@ describe('Given an instance of OnmsServer...', () => { expect(server.resolveURL('foo')).toEqual(SERVER_URL + 'foo'); expect(server.resolveURL('foo/')).toEqual(SERVER_URL + 'foo'); }); + it('URL starting with "/" are returned as-is.', () => { + expect(server.resolveURL('/rest/foo/')).toEqual('/rest/foo/'); + }); + it('Absolute with query appends query', () => { + expect(server.resolveURL('/rest/foo', 'foo=bar')).toEqual('/rest/foo?foo%3Dbar'); + }); + it('multi segment urls are handled.', () => { + expect(server.resolveURL('rest/foo/')).toEqual(SERVER_URL + 'rest/foo'); + }); + it('Colons are not escaped', () => { + expect(server.resolveURL('rest/foo/A:B:0.0.0.0:C')).toEqual(SERVER_URL + 'rest/foo/A:B:0.0.0.0:C'); + }); + it('Escape forward slashes', () => { + expect(server.resolveURL('rest/S%2FA%3AB%3A0.0.0.0%3AC')).toEqual(SERVER_URL + 'rest/S%2FA:B:0.0.0.0:C'); + }); it('it should have a "host" property', () => { expect(server.host).toBeDefined(); expect(server.host).toEqual('demo.opennms.org'); diff --git a/test/dao/SituationFeedbackDAO.spec.ts b/test/dao/SituationFeedbackDAO.spec.ts new file mode 100644 index 000000000..83d33601a --- /dev/null +++ b/test/dao/SituationFeedbackDAO.spec.ts @@ -0,0 +1,56 @@ +// tslint:disable-next-line:one-variable-per-declaration +declare const await, describe, beforeEach, it, xit, expect, jest; + +import { log, catRoot, setLogLevel } from '../../src/api/Log'; +import { LogLevel } from 'typescript-logging'; + +import { Client } from '../../src/Client'; + +import { OnmsAuthConfig } from '../../src/api/OnmsAuthConfig'; +import { OnmsServer } from '../../src/api/OnmsServer'; +import { OnmsResult } from '../../src/api/OnmsResult'; + +import { Comparators } from '../../src/api/Comparator'; +import { Filter } from '../../src/api/Filter'; +import { Restriction } from '../../src/api/Restriction'; +import { SearchPropertyTypes } from '../../src/api/SearchPropertyType'; + +import { SituationFeedbackDAO } from '../../src/dao/SituationFeedbackDAO'; + +import { OnmsSituationFeedback } from '../../src/model/OnmsSituationFeedback'; + +import { MockHTTP23 } from '../rest/MockHTTP23'; +import { OnmsSituationFeedbackType } from '../../src/model/OnmsSituationFeedbackType'; + +const SERVER_NAME = 'Demo'; +const SERVER_URL = 'http://demo.opennms.org/opennms/'; +const SERVER_USER = 'demo'; +const SERVER_PASSWORD = 'demo'; + +// tslint:disable-next-line:one-variable-per-declaration +let opennms: Client, server, auth, mockHTTP, dao: SituationFeedbackDAO; + +describe('SituationfeedbackDAO with v1 API', () => { + beforeEach((done) => { + auth = new OnmsAuthConfig(SERVER_USER, SERVER_PASSWORD); + server = new OnmsServer(SERVER_NAME, SERVER_URL, auth); + mockHTTP = new MockHTTP23(server); + opennms = new Client(mockHTTP); + dao = new SituationFeedbackDAO(mockHTTP); + Client.getMetadata(server, mockHTTP).then((metadata) => { + server.metadata = metadata; + done(); + }); + }); + it('SituationFeedbackDAO.get(210)', () => { + return dao.getFeedback(210).then((feedback) => { + expect(feedback).toHaveLength(4); + expect(feedback[0].alarmKey).toEqual('uei.opennms.org/alarms/trigger:localhost:0.0.0.0:FEEDBACK_C'); + expect(feedback[0].fingerprint).toEqual('NDg3ZjdiMjJmNjgzMTJkMmMxYmJjOTNiMWFlYTQ0NWI='); + expect(feedback[0].feedbackType).toEqual(OnmsSituationFeedbackType.forId('CORRECT')); + expect(feedback[0].reason).toEqual('ALL_CORRECT'); + expect(feedback[0].user).toEqual('admin'); + expect(feedback[0].timestamp).toEqual(1533835399918); + }); + }); +}); diff --git a/test/rest/23.0.0/get/rest/situation-feedback/feedback.json b/test/rest/23.0.0/get/rest/situation-feedback/feedback.json new file mode 100644 index 000000000..f2ee5facf --- /dev/null +++ b/test/rest/23.0.0/get/rest/situation-feedback/feedback.json @@ -0,0 +1,38 @@ +[ + { + "situationKey": "uei.opennms.org/alarms/trigger:localhost:0.0.0.0:FEEDBACK_F", + "situationFingerprint": "NDg3ZjdiMjJmNjgzMTJkMmMxYmJjOTNiMWFlYTQ0NWI=", + "alarmKey": "uei.opennms.org/alarms/trigger:localhost:0.0.0.0:FEEDBACK_C", + "feedbackType": "CORRECT", + "reason": "ALL_CORRECT", + "user": "admin", + "timestamp": 1533835399918 + }, + { + "situationKey": "uei.opennms.org/alarms/trigger:localhost:0.0.0.0:FEEDBACK_F", + "situationFingerprint": "NDg3ZjdiMjJmNjgzMTJkMmMxYmJjOTNiMWFlYTQ0NWI=", + "alarmKey": "uei.opennms.org/alarms/trigger:localhost:0.0.0.0:FEEDBACK_A", + "feedbackType": "CORRECT", + "reason": "ALL_CORRECT", + "user": "admin", + "timestamp": 1533835399918 + }, + { + "situationKey": "uei.opennms.org/alarms/trigger:localhost:0.0.0.0:FEEDBACK_F", + "situationFingerprint": "NDg3ZjdiMjJmNjgzMTJkMmMxYmJjOTNiMWFlYTQ0NWI=", + "alarmKey": "uei.opennms.org/alarms/trigger:localhost:0.0.0.0:FEEDBACK_A", + "feedbackType": "CORRECT", + "reason": "ALL_CORRECT", + "user": "admin", + "timestamp": 1533835359151 + }, + { + "situationKey": "uei.opennms.org/alarms/trigger:localhost:0.0.0.0:FEEDBACK_F", + "situationFingerprint": "NDg3ZjdiMjJmNjgzMTJkMmMxYmJjOTNiMWFlYTQ0NWI=", + "alarmKey": "uei.opennms.org/alarms/trigger:localhost:0.0.0.0:FEEDBACK_C", + "feedbackType": "CORRECT", + "reason": "ALL_CORRECT", + "user": "admin", + "timestamp": 1533835359151 + } +] \ No newline at end of file diff --git a/test/rest/MockHTTP23.ts b/test/rest/MockHTTP23.ts index 9649b6e4e..30ba6a4d7 100644 --- a/test/rest/MockHTTP23.ts +++ b/test/rest/MockHTTP23.ts @@ -32,6 +32,11 @@ export class MockHTTP23 extends AbstractHTTP { result.type = 'application/json'; return Promise.resolve(result); } + case 'rest/situation-feedback/210': { + const result = OnmsResult.ok(require('./23.0.0/get/rest/situation-feedback/feedback.json')); + result.type = 'application/json'; + return Promise.resolve(result); + } } throw new Error('Not yet implemented: GET ' + urlObj.toString());