diff --git a/src/datasource-zabbix/specs/datasource.spec.js b/src/datasource-zabbix/specs/datasource.spec.js index a1063dc..f03da65 100644 --- a/src/datasource-zabbix/specs/datasource.spec.js +++ b/src/datasource-zabbix/specs/datasource.spec.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import mocks from '../../test-setup/mocks'; import { Datasource } from "../module"; import { zabbixTemplateFormat } from "../datasource"; @@ -17,15 +18,11 @@ describe('ZabbixDatasource', () => { dbConnectionEnable: false } }; - ctx.templateSrv = {}; - ctx.backendSrv = { - datasourceRequest: jest.fn() - }; - ctx.datasourceSrv = {}; - ctx.zabbixAlertingSrv = { - setPanelAlertState: jest.fn(), - removeZabbixThreshold: jest.fn(), - }; + + ctx.templateSrv = mocks.templateSrvMock; + ctx.backendSrv = mocks.backendSrvMock; + ctx.datasourceSrv = mocks.datasourceSrvMock; + ctx.zabbixAlertingSrv = mocks.zabbixAlertingSrvMock; ctx.ds = new Datasource(ctx.instanceSettings, ctx.templateSrv, ctx.backendSrv, ctx.datasourceSrv, ctx.zabbixAlertingSrv); }); diff --git a/src/datasource-zabbix/specs/dbConnector.test.js b/src/datasource-zabbix/specs/dbConnector.test.js index 31d5d08..0f8a302 100644 --- a/src/datasource-zabbix/specs/dbConnector.test.js +++ b/src/datasource-zabbix/specs/dbConnector.test.js @@ -1,16 +1,12 @@ +import mocks from '../../test-setup/mocks'; import DBConnector from '../zabbix/connectors/dbConnector'; describe('DBConnector', () => { let ctx = {}; - const backendSrvMock = {}; - const datasourceSrvMock = { - loadDatasource: jest.fn().mockResolvedValue( - { id: 42, name: 'foo', meta: {} } - ), - getAll: jest.fn().mockReturnValue([ - { id: 42, name: 'foo' } - ]) - }; + const backendSrv = mocks.backendSrvMock; + const datasourceSrv = mocks.datasourceSrvMock; + datasourceSrv.loadDatasource.mockResolvedValue({ id: 42, name: 'foo', meta: {} }); + datasourceSrv.getAll.mockReturnValue([{ id: 42, name: 'foo' }]); describe('When init DB connector', () => { beforeEach(() => { @@ -24,28 +20,28 @@ describe('DBConnector', () => { ctx.options = { datasourceName: 'bar' }; - const dbConnector = new DBConnector(ctx.options, backendSrvMock, datasourceSrvMock); + const dbConnector = new DBConnector(ctx.options, backendSrv, datasourceSrv); dbConnector.loadDBDataSource(); - expect(datasourceSrvMock.getAll).not.toHaveBeenCalled(); - expect(datasourceSrvMock.loadDatasource).toHaveBeenCalledWith('bar'); + expect(datasourceSrv.getAll).not.toHaveBeenCalled(); + expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('bar'); }); it('should load datasource by id if name not present', () => { - const dbConnector = new DBConnector(ctx.options, backendSrvMock, datasourceSrvMock); + const dbConnector = new DBConnector(ctx.options, backendSrv, datasourceSrv); dbConnector.loadDBDataSource(); - expect(datasourceSrvMock.getAll).toHaveBeenCalled(); - expect(datasourceSrvMock.loadDatasource).toHaveBeenCalledWith('foo'); + expect(datasourceSrv.getAll).toHaveBeenCalled(); + expect(datasourceSrv.loadDatasource).toHaveBeenCalledWith('foo'); }); it('should throw error if no name and id specified', () => { ctx.options = {}; - const dbConnector = new DBConnector(ctx.options, backendSrvMock, datasourceSrvMock); + const dbConnector = new DBConnector(ctx.options, backendSrv, datasourceSrv); return expect(dbConnector.loadDBDataSource()).rejects.toBe('SQL Data Source name should be specified'); }); it('should throw error if datasource with given id is not found', () => { ctx.options.datasourceId = 45; - const dbConnector = new DBConnector(ctx.options, backendSrvMock, datasourceSrvMock); + const dbConnector = new DBConnector(ctx.options, backendSrv, datasourceSrv); return expect(dbConnector.loadDBDataSource()).rejects.toBe('SQL Data Source with ID 45 not found'); }); }); diff --git a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js index 3432669..adefea1 100644 --- a/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js +++ b/src/datasource-zabbix/zabbix/connectors/zabbix_api/zabbixAPIConnector.js @@ -346,7 +346,7 @@ export class ZabbixAPIConnector { value: 1 }, selectGroups: ['name'], - selectHosts: ['name', 'host', 'maintenance_status'], + selectHosts: ['name', 'host', 'maintenance_status', 'proxy_hostid'], selectItems: ['name', 'key_', 'lastvalue'], selectLastEvent: 'extend', selectTags: 'extend' @@ -463,6 +463,14 @@ export class ZabbixAPIConnector { return triggers; }); } + + getProxies() { + var params = { + output: ['proxyid', 'host'], + }; + + return this.request('proxy.get', params); + } } function filterTriggersByAcknowledge(triggers, acknowledged) { diff --git a/src/datasource-zabbix/zabbix/zabbix.js b/src/datasource-zabbix/zabbix/zabbix.js index 6badc49..3920cd3 100644 --- a/src/datasource-zabbix/zabbix/zabbix.js +++ b/src/datasource-zabbix/zabbix/zabbix.js @@ -8,16 +8,16 @@ import { ZabbixNotImplemented } from './connectors/dbConnector'; const REQUESTS_TO_PROXYFY = [ 'getHistory', 'getTrend', 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs', - 'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getSLA', 'getVersion' + 'getEvents', 'getAlerts', 'getHostAlerts', 'getAcknowledges', 'getITService', 'getSLA', 'getVersion', 'getProxies' ]; const REQUESTS_TO_CACHE = [ - 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs', 'getITService' + 'getGroups', 'getHosts', 'getApps', 'getItems', 'getMacros', 'getItemsByIDs', 'getITService', 'getProxies' ]; const REQUESTS_TO_BIND = [ 'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts', - 'getAcknowledges', 'getITService', 'getVersion', 'login', 'acknowledgeEvent' + 'getAcknowledges', 'getITService', 'getVersion', 'login', 'acknowledgeEvent', 'getProxies' ]; export class Zabbix { @@ -243,7 +243,7 @@ export class Zabbix { /** * Build query - convert target filters to array of Zabbix items */ - getTriggers(groupFilter, hostFilter, appFilter, options) { + getTriggers(groupFilter, hostFilter, appFilter, options, proxyFilter) { let promises = [ this.getGroups(groupFilter), this.getHosts(groupFilter, hostFilter), @@ -252,9 +252,7 @@ export class Zabbix { return Promise.all(promises) .then(results => { - let filteredGroups = results[0]; - let filteredHosts = results[1]; - let filteredApps = results[2]; + let [filteredGroups, filteredHosts, filteredApps] = results; let query = {}; if (appFilter) { @@ -268,8 +266,36 @@ export class Zabbix { } return query; - }).then(query => { - return this.zabbixAPI.getTriggers(query.groupids, query.hostids, query.applicationids, options); + }) + .then(query => this.zabbixAPI.getTriggers(query.groupids, query.hostids, query.applicationids, options)) + .then(triggers => this.filterTriggersByProxy(triggers, proxyFilter)); + } + + filterTriggersByProxy(triggers, proxyFilter) { + return this.getFilteredProxies(proxyFilter) + .then(proxies => { + if (proxyFilter && proxyFilter !== '/.*/' && triggers) { + const proxy_ids = proxies.map(proxy => proxy.proxyid); + triggers = triggers.filter(trigger => { + let filtered = false; + for(let i = 0; i < trigger.hosts.length; i++) { + const host = trigger.hosts[i]; + if (proxy_ids.includes(host.proxy_hostid)) { + filtered = true; + } + } + return filtered; + }); + } + return triggers; + }); + } + + getFilteredProxies(proxyFilter) { + return this.zabbixAPI.getProxies() + .then(proxies => { + proxies.forEach(proxy => proxy.name = proxy.host); + return findByFilter(proxies, proxyFilter); }); } diff --git a/src/datasource-zabbix/zabbix/zabbix.test.js b/src/datasource-zabbix/zabbix/zabbix.test.js new file mode 100644 index 0000000..44b27c1 --- /dev/null +++ b/src/datasource-zabbix/zabbix/zabbix.test.js @@ -0,0 +1,98 @@ +import mocks from '../../test-setup/mocks'; +import { Zabbix } from './zabbix'; + +describe('Zabbix', () => { + let ctx = {}; + let zabbix; + let options = { + url: 'http://localhost', + username: 'zabbix', + password: 'zabbix', + zabbixVersion: 4, + }; + + beforeEach(() => { + ctx.options = options; + ctx.backendSrv = mocks.backendSrvMock; + ctx.datasourceSrv = mocks.datasourceSrvMock; + zabbix = new Zabbix(ctx.options, ctx.backendSrvMock, ctx.datasourceSrvMock); + }); + + describe('When querying proxies', () => { + beforeEach(() => { + zabbix.zabbixAPI.getProxies = jest.fn().mockResolvedValue([ + { host: 'proxy-foo', proxyid: '10101' }, + { host: 'proxy-bar', proxyid: '10102' }, + ]); + }); + + it("should return all proxies if filter set to /.*/", done => { + zabbix.getFilteredProxies('/.*/').then(proxies => { + expect(proxies).toMatchObject([{ host: 'proxy-foo' }, { host: 'proxy-bar' }]); + done(); + }); + }); + + it("should return matched proxies if regex filter used", done => { + zabbix.getFilteredProxies('/.*-foo/').then(proxies => { + expect(proxies).toMatchObject([{ host: 'proxy-foo' }]); + done(); + }); + }); + + it("should return matched proxies if simple filter used", done => { + zabbix.getFilteredProxies('proxy-bar').then(proxies => { + expect(proxies).toMatchObject([{ host: 'proxy-bar' }]); + done(); + }); + }); + + it("should return empty list for empty filter", done => { + zabbix.getFilteredProxies('').then(proxies => { + expect(proxies).toEqual([]); + done(); + }); + }); + }); + + describe('When filtering triggers by proxy', () => { + beforeEach(() => { + zabbix.zabbixAPI.getProxies = jest.fn().mockResolvedValue([ + { host: 'proxy-foo', proxyid: '10101' }, + { host: 'proxy-bar', proxyid: '10102' }, + ]); + ctx.triggers = [ + { triggerid: '1', hosts: [{ name: 'backend01', proxy_hostid: '0' }] }, + { triggerid: '2', hosts: [{ name: 'backend02', proxy_hostid: '0' }] }, + { triggerid: '3', hosts: [{ name: 'frontend01', proxy_hostid: '10101' }] }, + { triggerid: '4', hosts: [{ name: 'frontend02', proxy_hostid: '10101' }] }, + { triggerid: '5', hosts: [{ name: 'db01', proxy_hostid: '10102' }] }, + { triggerid: '6', hosts: [{ name: 'db02', proxy_hostid: '10102' }] }, + ]; + }); + + it("should return all triggers for empty filter", done => { + zabbix.filterTriggersByProxy(ctx.triggers, '').then(triggers => { + const triggerids = triggers.map(t => t.triggerid); + expect(triggerids).toEqual(['1', '2', '3', '4', '5', '6']); + done(); + }); + }); + + it("should return triggers belonging proxy matched regex filter", done => { + zabbix.filterTriggersByProxy(ctx.triggers, '/.*-foo/').then(triggers => { + const triggerids = triggers.map(t => t.triggerid); + expect(triggerids).toEqual(['3', '4']); + done(); + }); + }); + + it("should return triggers belonging proxy matched name filter", done => { + zabbix.filterTriggersByProxy(ctx.triggers, 'proxy-bar').then(triggers => { + const triggerids = triggers.map(t => t.triggerid); + expect(triggerids).toEqual(['5', '6']); + done(); + }); + }); + }); +}); diff --git a/src/panel-triggers/migrations.js b/src/panel-triggers/migrations.js index ec54371..8242c0c 100644 --- a/src/panel-triggers/migrations.js +++ b/src/panel-triggers/migrations.js @@ -2,7 +2,7 @@ import _ from 'lodash'; import {DEFAULT_TARGET} from './triggers_panel_ctrl'; // Actual schema version -export const CURRENT_SCHEMA_VERSION = 4; +export const CURRENT_SCHEMA_VERSION = 5; export function migratePanelSchema(panel) { if (isEmptyPanel(panel)) { @@ -31,7 +31,7 @@ export function migratePanelSchema(panel) { delete panel.hideHostsInMaintenance; } - if (schemaVersion < 4) { + if (schemaVersion < 5) { if (panel.targets && !_.isEmpty(panel.targets)) { _.each(panel.targets, (target) => { _.defaultsDeep(target, DEFAULT_TARGET); diff --git a/src/panel-triggers/partials/options_tab.html b/src/panel-triggers/partials/options_tab.html index 72b695a..c01feef 100644 --- a/src/panel-triggers/partials/options_tab.html +++ b/src/panel-triggers/partials/options_tab.html @@ -19,6 +19,12 @@ checked="ctrl.panel.hostGroups" on-change="ctrl.render()"> + +
{{ ds }}
-
+
+
+ + +
-
+
- diff --git a/src/panel-triggers/triggers_panel_ctrl.js b/src/panel-triggers/triggers_panel_ctrl.js index 9362ffb..6edcf87 100644 --- a/src/panel-triggers/triggers_panel_ctrl.js +++ b/src/panel-triggers/triggers_panel_ctrl.js @@ -15,6 +15,7 @@ export const DEFAULT_TARGET = { application: {filter: ""}, trigger: {filter: ""}, tags: {filter: ""}, + proxy: {filter: ""}, }; export const DEFAULT_SEVERITY = [ @@ -36,6 +37,7 @@ export const PANEL_DEFAULTS = { hostField: true, hostTechNameField: false, hostGroups: false, + hostProxy: false, showTags: true, statusField: true, severityField: true, @@ -203,31 +205,37 @@ export class TriggerPanelCtrl extends PanelCtrl { getTriggers() { let promises = _.map(this.panel.datasources, (ds) => { + let proxies; return this.datasourceSrv.get(ds) .then(datasource => { - var zabbix = datasource.zabbix; - var showEvents = this.panel.showEvents.value; - var triggerFilter = this.panel.targets[ds]; + const zabbix = datasource.zabbix; + const showEvents = this.panel.showEvents.value; + const triggerFilter = this.panel.targets[ds]; + const showProxy = this.panel.hostProxy; + const getProxiesPromise = showProxy ? zabbix.getProxies() : () => []; // Replace template variables - var groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter); - var hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter); - var appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter); + const groupFilter = datasource.replaceTemplateVars(triggerFilter.group.filter); + const hostFilter = datasource.replaceTemplateVars(triggerFilter.host.filter); + const appFilter = datasource.replaceTemplateVars(triggerFilter.application.filter); + const proxyFilter = datasource.replaceTemplateVars(triggerFilter.proxy.filter); let triggersOptions = { showTriggers: showEvents }; - return zabbix.getTriggers(groupFilter, hostFilter, appFilter, triggersOptions); - }).then((triggers) => { + return Promise.all([ + zabbix.getTriggers(groupFilter, hostFilter, appFilter, triggersOptions, proxyFilter), + getProxiesPromise + ]); + }).then(([triggers, sourceProxies]) => { + proxies = _.keyBy(sourceProxies, 'proxyid'); return this.getAcknowledges(triggers, ds); - }).then((triggers) => { - return this.setMaintenanceStatus(triggers); - }).then((triggers) => { - return this.filterTriggersPre(triggers, ds); - }).then((triggers) => { - return this.addTriggerDataSource(triggers, ds); - }); + }) + .then(triggers => this.setMaintenanceStatus(triggers)) + .then(triggers => this.filterTriggersPre(triggers, ds)) + .then(triggers => this.addTriggerDataSource(triggers, ds)) + .then(triggers => this.addTriggerHostProxy(triggers, proxies)); }); return Promise.all(promises) @@ -344,6 +352,19 @@ export class TriggerPanelCtrl extends PanelCtrl { return triggers; } + addTriggerHostProxy(triggers, proxies) { + triggers.forEach(trigger => { + if (trigger.hosts && trigger.hosts.length) { + let host = trigger.hosts[0]; + if (host.proxy_hostid !== '0') { + const hostProxy = proxies[host.proxy_hostid]; + host.proxy = hostProxy ? hostProxy.host : ''; + } + } + }); + return triggers; + } + sortTriggers(triggerList) { if (this.panel.sortTriggersBy.value === 'priority') { triggerList = _.orderBy(triggerList, ['priority', 'lastchangeUnix', 'triggerid'], ['desc', 'desc', 'desc']); @@ -355,12 +376,15 @@ export class TriggerPanelCtrl extends PanelCtrl { formatTrigger(zabbixTrigger) { let trigger = _.cloneDeep(zabbixTrigger); - let triggerObj = trigger; - // Set host that the trigger belongs + // Set host and proxy that the trigger belongs if (trigger.hosts && trigger.hosts.length) { - triggerObj.host = trigger.hosts[0].name; - triggerObj.hostTechName = trigger.hosts[0].host; + const host = trigger.hosts[0]; + trigger.host = host.name; + trigger.hostTechName = host.host; + if (host.proxy) { + trigger.proxy = host.proxy; + } } // Set tags if present @@ -375,9 +399,9 @@ export class TriggerPanelCtrl extends PanelCtrl { // Format last change and age trigger.lastchangeUnix = Number(trigger.lastchange); - triggerObj = this.setTriggerLastChange(triggerObj); - triggerObj = this.setTriggerSeverity(triggerObj); - return triggerObj; + trigger = this.setTriggerLastChange(trigger); + trigger = this.setTriggerSeverity(trigger); + return trigger; } updateTriggerFormat(trigger) { @@ -491,6 +515,9 @@ export class TriggerPanelCtrl extends PanelCtrl { } else if (this.panel.hostField || this.panel.hostTechNameField) { host = this.panel.hostField ? trigger.host : trigger.hostTechName; } + if (this.panel.hostProxy && trigger.proxy) { + host = `${trigger.proxy}: ${host}`; + } return host; } diff --git a/src/panel-triggers/triggers_tab.js b/src/panel-triggers/triggers_tab.js index daea603..c3ba5ba 100644 --- a/src/panel-triggers/triggers_tab.js +++ b/src/panel-triggers/triggers_tab.js @@ -17,6 +17,7 @@ class TriggersTabCtrl { getGroupNames: {}, getHostNames: {}, getApplicationNames: {}, + getProxyNames: {}, oldTarget: _.cloneDeep(this.panel.targets) }; _.defaultsDeep(this, scopeDefaults); @@ -40,6 +41,7 @@ class TriggersTabCtrl { this.getGroupNames[ds] = _.bind(this.suggestGroups, this, datasource); this.getHostNames[ds] = _.bind(this.suggestHosts, this, datasource); this.getApplicationNames[ds] = _.bind(this.suggestApps, this, datasource); + this.getProxyNames[ds] = _.bind(this.suggestProxies, this, datasource); } suggestGroups(datasource, query, callback) { @@ -69,6 +71,12 @@ class TriggersTabCtrl { .then(callback); } + suggestProxies(datasource, query, callback) { + return datasource.zabbix.getProxies() + .then(proxies => _.map(proxies, 'host')) + .then(callback); + } + datasourcesChanged() { _.each(this.panel.datasources, (ds) => { if (!this.panel.targets[ds]) { @@ -84,8 +92,8 @@ class TriggersTabCtrl { var newTarget = _.cloneDeep(this.panel.targets); if (!_.isEqual(this.oldTarget, newTarget)) { this.oldTarget = newTarget; + this.panelCtrl.refresh(); } - this.panelCtrl.refresh(); }); } diff --git a/src/test-setup/mocks.js b/src/test-setup/mocks.js new file mode 100644 index 0000000..efd8e43 --- /dev/null +++ b/src/test-setup/mocks.js @@ -0,0 +1,26 @@ +export let templateSrvMock = { + replace: jest.fn().mockImplementation(query => query) +}; + +export let backendSrvMock = { + datasourceRequest: jest.fn() +}; + +export let datasourceSrvMock = { + loadDatasource: jest.fn(), + getAll: jest.fn() +}; + +export let zabbixAlertingSrvMock = { + setPanelAlertState: jest.fn(), + removeZabbixThreshold: jest.fn(), +}; + +const defaultExports = { + templateSrvMock, + backendSrvMock, + datasourceSrvMock, + zabbixAlertingSrvMock +}; + +export default defaultExports;