Execute scripts from problem details

This commit is contained in:
Alexander Zobnin
2020-05-27 15:15:36 +03:00
parent 092acec295
commit 6841fa0386
7 changed files with 114 additions and 88 deletions

View File

@@ -54,4 +54,7 @@ export interface ZBXScript {
execute_on?: string; execute_on?: string;
} }
export type APIScriptGetResponse = ZabbixAPIResponse<ZBXScript[]>; export interface APIExecuteScriptResponse {
response: 'success' | 'failed';
value?: string;
}

View File

@@ -5,7 +5,7 @@ import * as utils from '../../../utils';
import { ZabbixAPICore } from './zabbixAPICore'; import { ZabbixAPICore } from './zabbixAPICore';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants'; import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants';
import { ShowProblemTypes, ZBXProblem } from '../../../types'; import { ShowProblemTypes, ZBXProblem } from '../../../types';
import { JSONRPCRequestParams, APIScriptGetResponse } from './types'; import { JSONRPCRequestParams, ZBXScript, APIExecuteScriptResponse } from './types';
const DEFAULT_ZABBIX_VERSION = '3.0.0'; const DEFAULT_ZABBIX_VERSION = '3.0.0';
@@ -665,13 +665,22 @@ export class ZabbixAPIConnector {
return this.request('proxy.get', params); return this.request('proxy.get', params);
} }
getScripts(hostids: string[], options?: any): APIScriptGetResponse { getScripts(hostids: string[], options?: any): Promise<ZBXScript[]> {
const params: any = { const params: any = {
output: 'extend', output: 'extend',
hostids, hostids,
}; };
return this.request('script.get', params); return this.request('script.get', params).then(utils.mustArray);
}
executeScript(hostid: string, scriptid: string): Promise<APIExecuteScriptResponse> {
const params: any = {
hostid,
scriptid,
};
return this.request('script.execute', params);
} }
} }

View File

@@ -30,7 +30,7 @@ const REQUESTS_TO_CACHE = [
const REQUESTS_TO_BIND = [ const REQUESTS_TO_BIND = [
'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts', 'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts',
'getAcknowledges', 'getITService', 'getVersion', 'login', 'acknowledgeEvent', 'getProxies', 'getEventAlerts', 'getAcknowledges', 'getITService', 'getVersion', 'login', 'acknowledgeEvent', 'getProxies', 'getEventAlerts',
'getExtendedEventData', 'getScripts' 'getExtendedEventData', 'getScripts', 'executeScript',
]; ];
export class Zabbix implements ZabbixConnector { export class Zabbix implements ZabbixConnector {

View File

@@ -1,21 +1,17 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { cx, css } from 'emotion'; import { cx, css } from 'emotion';
import { ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_CHANGE_SEVERITY, ZBX_ACK_ACTION_CLOSE } from '../../datasource-zabbix/constants'; import { ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_CHANGE_SEVERITY, ZBX_ACK_ACTION_CLOSE } from '../../datasource-zabbix/constants';
import { APIScriptGetResponse, ZBXScript } from '../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; import { ZBXScript, APIExecuteScriptResponse } from '../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
import { Button, VerticalGroup, Spinner, Modal, Select, Forms, stylesFactory, withTheme, Themeable } from '@grafana/ui'; import { Button, Spinner, Modal, Select, stylesFactory, withTheme, Themeable } from '@grafana/ui';
import { FAIcon } from '../../components';
import * as grafanaUi from '@grafana/ui';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaTheme, SelectableValue } from '@grafana/data';
const Checkbox: any = Forms?.Checkbox || (grafanaUi as any).Checkbox; import { FAIcon } from '../../components';
const RadioButtonGroup: any = Forms?.RadioButtonGroup || (grafanaUi as any).RadioButtonGroup;
const KEYBOARD_ENTER_KEY = 13; const KEYBOARD_ENTER_KEY = 13;
const KEYBOARD_ESCAPE_KEY = 27; const KEYBOARD_ESCAPE_KEY = 27;
interface Props extends Themeable { interface Props extends Themeable {
getScripts(): Promise<APIScriptGetResponse>; getScripts(): Promise<ZBXScript[]>;
onSubmit(data?: AckProblemData): Promise<any> | any; onSubmit(data?: ExecScriptData): Promise<any> | any;
onDismiss?(): void; onDismiss?(): void;
} }
@@ -24,17 +20,14 @@ interface State {
scriptOptions: Array<SelectableValue<string>>; scriptOptions: Array<SelectableValue<string>>;
script: ZBXScript; script: ZBXScript;
error: boolean; error: boolean;
errorMessage: string; errorMessage: string | JSX.Element;
result: string | JSX.Element;
selectError: string; selectError: string;
result: string;
loading: boolean; loading: boolean;
} }
export interface AckProblemData { export interface ExecScriptData {
message: string; scriptid: string;
closeProblem?: boolean;
action?: number;
severity?: number;
} }
export class ExecScriptModalUnthemed extends PureComponent<Props, State> { export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
@@ -71,21 +64,9 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
this.setState({ scriptOptions, selectedScript, script }); this.setState({ scriptOptions, selectedScript, script });
} }
handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.which === KEYBOARD_ENTER_KEY || event.key === 'Enter') {
this.submit();
} else if (event.which === KEYBOARD_ESCAPE_KEY || event.key === 'Escape') {
this.dismiss();
}
}
handleBackdropClick = () => {
this.dismiss();
}
onChangeSelectedScript = (v: SelectableValue<string>) => { onChangeSelectedScript = (v: SelectableValue<string>) => {
const script = this.scripts.find(s => v.value === s.scriptid); const script = this.scripts.find(s => v.value === s.scriptid);
this.setState({ selectedScript: v, script }); this.setState({ selectedScript: v, script, errorMessage: '', loading: false, result: '' });
}; };
dismiss = () => { dismiss = () => {
@@ -94,55 +75,54 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
} }
submit = () => { submit = () => {
// const { acknowledge, changeSeverity, closeProblem } = this.state; const { selectedScript } = this.state;
// const actionSelected = acknowledge || changeSeverity || closeProblem; if (!selectedScript) {
// if (!this.state.value && !actionSelected) { return this.setState({
// return this.setState({ selectError: 'Select a script to execute.'
// error: true, });
// errorMessage: 'Enter message text or select an action' }
// });
// }
// this.setState({ ackError: '', loading: true }); this.setState({ errorMessage: '', loading: true, result: '' });
// const ackData: AckProblemData = { const data: ExecScriptData = {
// message: this.state.value, scriptid: selectedScript.value,
// }; };
// let action = ZBX_ACK_ACTION_ADD_MESSAGE; this.props.onSubmit(data).then((result: APIExecuteScriptResponse) => {
// if (this.state.acknowledge) { const message = this.formatResult(result?.value || '');
// action += ZBX_ACK_ACTION_ACK; if (result?.response === 'success') {
// } this.setState({ result: message, loading: false });
// if (this.state.changeSeverity) { } else {
// action += ZBX_ACK_ACTION_CHANGE_SEVERITY; this.setState({ error: true, errorMessage: message, loading: false });
// ackData.severity = this.state.selectedSeverity; }
// } }).catch(err => {
// if (this.state.closeProblem) { let errorMessage = err.message || err.data || '';
// action += ZBX_ACK_ACTION_CLOSE; errorMessage = this.formatResult(errorMessage);
// } this.setState({
// ackData.action = action; error: true,
loading: false,
errorMessage,
});
});
}
// this.props.onSubmit(ackData).then(() => { formatResult = (result: string) => {
// this.dismiss(); const formatted = result.split('\n').map((p, i) => {
// }).catch(err => { return <p key={i}>{p}</p>;
// this.setState({ });
// ackError: err.message || err.data, return <>{formatted}</>;
// loading: false,
// });
// });
} }
render() { render() {
const { theme } = this.props; const { theme } = this.props;
const { scriptOptions, selectedScript, script, selectError, errorMessage } = this.state; const { scriptOptions, selectedScript, script, result, selectError, errorMessage, error } = this.state;
const styles = getStyles(theme); const styles = getStyles(theme);
const modalClass = cx(styles.modal); const modalClass = cx(styles.modal);
const modalTitleClass = cx(styles.modalHeaderTitle); const modalTitleClass = cx(styles.modalHeaderTitle);
const inputGroupClass = cx('gf-form', styles.inputGroup); const selectErrorClass = cx('gf-form-hint-text', styles.inputError);
const inputHintClass = cx('gf-form-hint-text', styles.inputHint); const scriptCommandContainerClass = cx('gf-form', styles.scriptCommandContainer);
const inputErrorClass = cx('gf-form-hint-text', styles.inputError);
const scriptCommandClass = cx('gf-form-hint-text', styles.scriptCommand); const scriptCommandClass = cx('gf-form-hint-text', styles.scriptCommand);
return ( return (
@@ -157,28 +137,30 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
</div> </div>
} }
> >
<div className={inputGroupClass}> <div className="gf-form">
<label className="gf-form-hint"> <label className="gf-form-hint">
<Select <Select
options={scriptOptions} options={scriptOptions}
value={selectedScript} value={selectedScript}
onChange={this.onChangeSelectedScript} onChange={this.onChangeSelectedScript}
/> />
<small className={inputHintClass}>Press Enter to execute</small>
{selectError && {selectError &&
<small className={inputErrorClass}>{selectError}</small> <small className={selectErrorClass}>{selectError}</small>
} }
</label> </label>
</div> </div>
<div className="gf-form"> <div className={scriptCommandContainerClass}>
{script && <small className={scriptCommandClass}>{script.command}</small>} {script && <small className={scriptCommandClass}>{script.command}</small>}
</div> </div>
{this.state.error && <div className={styles.resultContainer}>
<div className="gf-form ack-request-error"> {result &&
<span className={styles.execError}>{errorMessage}</span> <span className={styles.execResult}>{result}</span>
</div>
} }
{error &&
<span className={styles.execError}>{errorMessage}</span>
}
</div>
<div className="gf-form-button-row text-center"> <div className="gf-form-button-row text-center">
<Button variant="primary" onClick={this.submit}>Execute</Button> <Button variant="primary" onClick={this.submit}>Execute</Button>
@@ -193,7 +175,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
const red = theme.colors.red || (theme as any).palette.red; const red = theme.colors.red || (theme as any).palette.red;
return { return {
modal: css` modal: css`
width: 500px; width: 600px;
`, `,
modalHeaderTitle: css` modalHeaderTitle: css`
font-size: ${theme.typography.heading.h3}; font-size: ${theme.typography.heading.h3};
@@ -201,16 +183,16 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
margin: 0 ${theme.spacing.md}; margin: 0 ${theme.spacing.md};
display: flex; display: flex;
`, `,
inputGroup: css`
`,
input: css` input: css`
border-color: ${red}; border-color: ${red};
border-radius: 2px; border-radius: 2px;
outline-offset: 2px; outline-offset: 2px;
box-shadow: 0 0 0 2px ${theme.colors.pageBg || (theme as any).colors.bg1}, 0 0 0px 4px ${red}; box-shadow: 0 0 0 2px ${theme.colors.pageBg || (theme as any).colors.bg1}, 0 0 0px 4px ${red};
`, `,
scriptCommandContainer: css`
margin-bottom: ${theme.spacing.md};
`,
scriptCommand: css` scriptCommand: css`
float: left;
color: ${theme.colors.textWeak}; color: ${theme.colors.textWeak};
text-align: left; text-align: left;
font-family: ${theme.typography.fontFamily.monospace}; font-family: ${theme.typography.fontFamily.monospace};
@@ -224,6 +206,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
float: left; float: left;
color: ${red}; color: ${red};
`, `,
resultContainer: css`
min-height: 50px;
font-family: ${theme.typography.fontFamily.monospace};
font-size: ${theme.typography.size.sm};
p {
font-size: ${theme.typography.size.sm};
margin-bottom: 0px;
}
`,
execResult: css`
`,
execError: css` execError: css`
color: ${red}; color: ${red};
`, `,

View File

@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import moment from 'moment'; import moment from 'moment';
import * as utils from '../../../datasource-zabbix/utils'; import * as utils from '../../../datasource-zabbix/utils';
import { ProblemDTO, ZBXHost, ZBXGroup, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types'; import { ProblemDTO, ZBXHost, ZBXGroup, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types';
import { ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; import { ZBXScript, APIExecuteScriptResponse } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
import { ZBXItem, GFTimeRange, RTRow } from '../../types'; import { ZBXItem, GFTimeRange, RTRow } from '../../types';
import { AckModal, AckProblemData } from '../AckModal'; import { AckModal, AckProblemData } from '../AckModal';
import EventTag from '../EventTag'; import EventTag from '../EventTag';
@@ -10,7 +10,7 @@ import ProblemStatusBar from './ProblemStatusBar';
import AcknowledgesList from './AcknowledgesList'; import AcknowledgesList from './AcknowledgesList';
import ProblemTimeline from './ProblemTimeline'; import ProblemTimeline from './ProblemTimeline';
import { FAIcon, ExploreButton, AckButton, Tooltip, ModalController, ExecScriptButton } from '../../../components'; import { FAIcon, ExploreButton, AckButton, Tooltip, ModalController, ExecScriptButton } from '../../../components';
import { ExecScriptModal } from '../ExecScriptModal'; import { ExecScriptModal, ExecScriptData } from '../ExecScriptModal';
interface ProblemDetailsProps extends RTRow<ProblemDTO> { interface ProblemDetailsProps extends RTRow<ProblemDTO> {
rootWidth: number; rootWidth: number;
@@ -20,6 +20,7 @@ interface ProblemDetailsProps extends RTRow<ProblemDTO> {
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>; getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>; getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>; getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>;
onExecuteScript(problem: ProblemDTO, scriptid: string): Promise<APIExecuteScriptResponse>;
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise<any> | any; onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise<any> | any;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
} }
@@ -82,6 +83,11 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
return this.props.getScripts(problem); return this.props.getScripts(problem);
} }
onExecuteScript = (data: ExecScriptData) => {
const problem = this.props.original as ProblemDTO;
return this.props.onExecuteScript(problem, data.scriptid);
}
render() { render() {
const problem = this.props.original as ProblemDTO; const problem = this.props.original as ProblemDTO;
const alerts = this.state.alerts; const alerts = this.state.alerts;
@@ -117,7 +123,7 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
onClick={() => { onClick={() => {
showModal(ExecScriptModal, { showModal(ExecScriptModal, {
getScripts: this.getScripts, getScripts: this.getScripts,
onSubmit: this.ackProblem, onSubmit: this.onExecuteScript,
onDismiss: hideModal, onDismiss: hideModal,
}); });
}} }}

View File

@@ -10,7 +10,7 @@ import { AckProblemData } from '../AckModal';
import { GFHeartIcon, FAIcon } from '../../../components'; import { GFHeartIcon, FAIcon } from '../../../components';
import { ProblemsPanelOptions, GFTimeRange, RTCell, TriggerSeverity, RTResized } from '../../types'; import { ProblemsPanelOptions, GFTimeRange, RTCell, TriggerSeverity, RTResized } from '../../types';
import { ProblemDTO, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types'; import { ProblemDTO, ZBXEvent, ZBXTag, ZBXAlert } from '../../../datasource-zabbix/types';
import { ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; import { ZBXScript, APIExecuteScriptResponse } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
import { AckCell } from './AckCell'; import { AckCell } from './AckCell';
export interface ProblemListProps { export interface ProblemListProps {
@@ -24,6 +24,7 @@ export interface ProblemListProps {
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>; getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>; getProblemAlerts: (problem: ProblemDTO) => Promise<ZBXAlert[]>;
getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>; getScripts: (problem: ProblemDTO) => Promise<ZBXScript[]>;
onExecuteScript: (problem: ProblemDTO, scriptid: string) => Promise<APIExecuteScriptResponse>;
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void; onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void;
onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void; onTagClick?: (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => void;
onPageSizeChange?: (pageSize: number, pageIndex: number) => void; onPageSizeChange?: (pageSize: number, pageIndex: number) => void;
@@ -55,6 +56,9 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
return this.props.onProblemAck(problem, data); return this.props.onProblemAck(problem, data);
} }
onExecuteScript = (problem: ProblemDTO, data: AckProblemData) => {
}
handlePageSizeChange = (pageSize, pageIndex) => { handlePageSizeChange = (pageSize, pageIndex) => {
if (this.props.onPageSizeChange) { if (this.props.onPageSizeChange) {
this.props.onPageSizeChange(pageSize, pageIndex); this.props.onPageSizeChange(pageSize, pageIndex);
@@ -174,6 +178,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
getProblemAlerts={this.props.getProblemAlerts} getProblemAlerts={this.props.getProblemAlerts}
getScripts={this.props.getScripts} getScripts={this.props.getScripts}
onProblemAck={this.handleProblemAck} onProblemAck={this.handleProblemAck}
onExecuteScript={this.props.onExecuteScript}
onTagClick={this.handleTagClick} onTagClick={this.handleTagClick}
subRows={false} subRows={false}
/> />

View File

@@ -317,11 +317,11 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl {
} }
getProblemScripts(problem: ProblemDTO) { getProblemScripts(problem: ProblemDTO) {
const hostIds = problem.hosts?.map(h => h.hostid); const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null;
return getDataSourceSrv().get(problem.datasource) return getDataSourceSrv().get(problem.datasource)
.then((datasource: any) => { .then((datasource: any) => {
return datasource.zabbix.getScripts(hostIds); return datasource.zabbix.getScripts([hostid]);
}); });
} }
@@ -361,6 +361,15 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl {
}); });
} }
executeScript(problem: ProblemDTO, scriptid: string) {
const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null;
return getDataSourceSrv().get(problem.datasource)
.then((datasource: any) => {
return datasource.zabbix.executeScript(hostid, scriptid);
});
}
handlePageSizeChange(pageSize, pageIndex) { handlePageSizeChange(pageSize, pageIndex) {
this.panel.pageSize = pageSize; this.panel.pageSize = pageSize;
this.pageIndex = pageIndex; this.pageIndex = pageIndex;
@@ -415,6 +424,7 @@ export class TriggerPanelCtrl extends MetricsPanelCtrl {
const { message, action, severity } = data; const { message, action, severity } = data;
return ctrl.acknowledgeProblem(trigger, message, action, severity); return ctrl.acknowledgeProblem(trigger, message, action, severity);
}, },
onExecuteScript: ctrl.executeScript.bind(ctrl),
onTagClick: (tag, datasource, ctrlKey, shiftKey) => { onTagClick: (tag, datasource, ctrlKey, shiftKey) => {
if (ctrlKey || shiftKey) { if (ctrlKey || shiftKey) {
ctrl.removeTagFilter(tag, datasource); ctrl.removeTagFilter(tag, datasource);