diff options
author | Vjaceslavs Bogdanovs <vjaceslavs.bogdanovs@zabbix.com> | 2021-05-06 09:02:30 +0300 |
---|---|---|
committer | Vjaceslavs Bogdanovs <vjaceslavs.bogdanovs@zabbix.com> | 2021-05-06 09:02:30 +0300 |
commit | 7aa8cf6251109910f70651774f290dc52cc0fc03 (patch) | |
tree | c4a6452cfadad90c48b39ec3c4996bde08205eef /templates | |
parent | c911991b3dd82cb91482e91f8b2dffbeb0a3914d (diff) | |
parent | b34b5b4da3f4db39c7a2fcf9d3f94b99f9214cdd (diff) |
.........T [ZBXNEXT-6592] added ManageEngine ServiceDesk media
Merge in ZBX/zabbix from feature/ZBXNEXT-6592-5.3 to master
* commit 'b34b5b4da3f4db39c7a2fcf9d3f94b99f9214cdd':
.........T [ZBXNEXT-6592] fixed minor typo in ManageEngine ServiceDesk media readme file
.........T [ZBXNEXT-6592] fixed readme
.........T [ZBXNEXT-6592] fixed udf_ fields naming
.........T [ZBXNEXT-6592] fixed recovery message
.........T [ZBXNEXT-6592] fixed after QA
.........T [ZBXNEXT-6592] fixed after review
.........T [ZBXNEXT-6592] added readme
.........T [ZBXNEXT-6592] added ManageEngine ServiceDesk media (tuskov)
Diffstat (limited to 'templates')
-rw-r--r-- | templates/media/manageengine_servicedesk/README.md | 89 | ||||
-rw-r--r-- | templates/media/manageengine_servicedesk/media_manageengine_servicedesk.yaml | 466 |
2 files changed, 555 insertions, 0 deletions
diff --git a/templates/media/manageengine_servicedesk/README.md b/templates/media/manageengine_servicedesk/README.md new file mode 100644 index 00000000000..5bf4a76f536 --- /dev/null +++ b/templates/media/manageengine_servicedesk/README.md @@ -0,0 +1,89 @@ +# ManageEngine ServiceDesk webhook + +This guide describes how to integrate Zabbix 5.4 installation with ManageEngine ServiceDesk (both on-premise and on-demand) using the Zabbix webhook feature. This guide provides instructions on setting up a media type, a user and an action in Zabbix.<br> +Please note that recovery and update operations are supported only for trigger-based events. + +## Setting up ManageEngine ServiceDesk +At first, create a user for API or use an existing one. + +### Setting up the on-premise installation +1\. Go to *Admin -> Technicians*.<br> +2\. Click the *Add New Technician* link, enter the Technician details and provide login permission.<br> +3\. Click *Generate link* under the API key details block. Select a time frame for the key to expire using the Calendar icon, or simply retain the same key perpetually.<br> +4\. Save TEHNICAN_KEY for use in Zabbix later.<br> + +### Setting up the on-demand installation +1\. Go to [Zoho Developer Console](https://api-console.zoho.com/).<br> +2\. Choose *Self Client* from the list of client types, and click *Create Now*.<br> +3\. Click OK in the pop up to enable a self client for your account.<br> +4\. Now, your *Client ID* and *Client secret* are displayed under the *Client Secret* tab. Save them for use in Zabbix later.<br> +5\. Click the *Generate Code* tab and enter the ***SDPOnDemand.requests.ALL*** scope.<br> +6\. Select the *Time Duration* for which the grant token is valid. Please note that after this time, the grant token expires.<br> +7\. Enter a description and click *Generate*.<br> +8\. The generated code for the specified scope is displayed. Copy the grant code.<br> +9\. Make a POST request with the URL params following parameters:<br> +- **code**: enter the Grant Token / Authorization Code generated from previous step. +- **grant_type**: enter the value as "authorization_code". +- **client_id**: specify client_id obtained in step 4. +- **client_secret**: specify client_secret obtained in step 4. +- **redirect_uri**: specify the Callback URL that you registered during the app registration. You can use any ULR for Self client mode, *https://www.zoho.com* for example. + +Example: +``` +curl -X POST 'https://accounts.zoho.com/oauth/v2/token?code=1000.f74e7b6fc16c95bbc1fa2f067962f84b.9768e796b6273774817032613ba6892a&grant_type=authorization_code&client_id=1000.15S25B602CISR5WO9RUZ8UT39O3RIH&client_secret=9ea302935eb150d9d6cbefd35b1eb8891332d815b8&redirect_uri=https://www.zoho.com' +``` +Use your domain-specific Zoho accounts URL when you make the request.<br> +- For US: https://accounts.zoho.com +- For AU: https://accounts.zoho.com.au +- For EU: https://accounts.zoho.eu +- For IN: https://accounts.zoho.in +- For CN: https://accounts.zoho.com.cn + +10\. If the request is successful, you will receive the following output:<br> +```{ “access_token”: “1000.2370ff1fd75e968ae780cd8d14841e82.03518d2d1dab9c6c4cf74ae82b89defa”, “refresh_token”: “1000.2afabf2f5a396325e88f715c6de34d12.edce6130ca3832a14e5f80d005a5324d”, “token_type”: “Bearer”, “expires_in”: 3600 }```<br> +Save the *refresh_token* for using in Zabbix later. + +## Setting up the webhook in Zabbix +1\. In the *Administration > Media types* section, import [media_manageengine_servicedesk.yaml](media_manageengine_servicedesk.yaml). + +2\. Open the newly added **ManageEngine ServiceDesk** media type and replace all *<PLACEHOLDERS>* with your values.<br> + +The following parameters are required for on-premise ServiceDesk:<br> +**sd_on_premise** - *true*.<br> +**sd_url** - the URL of your instance.<br> +**sd_on_premise_auth_token** - the TEHNICAN_KEY generated earlier.<br> +**field_ref:requester** - login of the account used for request creation.<br> + +The following parameters are required for on-demand ServiceDesk:<br> +**sd_on_premise** - *false**.<br> +**sd_url** - the URL of your instance.<br> +**sd_on_demand_url_auth** - your domain-specific Zoho accounts URL for refreshing access token.<br> +**sd_on_demand_client_id**, **sd_on_demand_client_secret**, **sd_on_demand_refresh_token** - created earlier authentication details.<br> +**field_ref:requester** - requester's displaying name. You can remove this parameter or use any name. *"Zabbix"*, for example. <br> + +3\. Create a **Zabbix user** and add **Media** with the **ManageEngine ServiceDesk** media type. +Though a "Send to" field is not used in ManageEngine ServiceDesk webhook, it cannot be empty. To comply with frontend requirements, you can put any symbol there. +Make sure this user has access to all hosts, for which you would like problem notifications to be converted into ManageEngine ServiceDesk tasks. + +## Customize your requests +You can add any data to ServiceDesk or user-defined fields.<br> +Please see the [On-demand](https://www.manageengine.com/products/service-desk/sdpod-v3-api/SDPOD-V3-API.html#add-request) and [On-premise]( +https://ui.servicedeskplus.com/APIDocs3/index.html#add-request) API specification for details about fields.<br> +Most of fields should be filled as single-line string, other should be an object with *name* property. Zabbix can fill both, but not *"date"* fields.<br> +Supported field types: Single-line, Multi-line, Numeric, Pick List, Email, Phone, Currency, Decimal, Percent, Web URL, Radio Button, Decision Box. All of them should be passed as string.<br> + +Fields should be in format **field_string:fieldname**, where:<br> +**field** - can be *field* for system fields or *udf_field* for user-defined fields. The prefix for payload generator.<br> +**string** - should be *string* for single-line strings or any other for *REFERRED_FIELD*.<br> +**:** - separator between prefix and field name.<br> +**fieldname** - the name of ServiceDesk or user-defined field.<br> +Examples:<br> +`field_string:subject`<br> +`field_ref:template`<br> +`udf_field_string:udf_char1` + + +For more information see [Zabbix](https://www.zabbix.com/documentation/5.4/manual/config/notifications) and [ManageEngine ServiceDesk](https://www.manageengine.com/products/service-desk/support.html) documentations. + +## Supported versions +Zabbix 5.4 and higher diff --git a/templates/media/manageengine_servicedesk/media_manageengine_servicedesk.yaml b/templates/media/manageengine_servicedesk/media_manageengine_servicedesk.yaml new file mode 100644 index 00000000000..408c0427091 --- /dev/null +++ b/templates/media/manageengine_servicedesk/media_manageengine_servicedesk.yaml @@ -0,0 +1,466 @@ +zabbix_export: + version: '5.4' + date: '2021-04-26T13:40:40Z' + media_types: + - + name: 'ManageEngine ServiceDesk' + type: WEBHOOK + parameters: + 22: + name: event_nseverity + value: '{EVENT.NSEVERITY}' + 0: + name: event_recovery_value + value: '{EVENT.RECOVERY.VALUE}' + 1: + name: event_source + value: '{EVENT.SOURCE}' + 2: + name: event_update_status + value: '{EVENT.UPDATE.STATUS}' + 3: + name: event_value + value: '{EVENT.VALUE}' + 14: + name: 'field_ref:requester' + value: '<PLACE API USER NAME>' + 11: + name: 'field_string:description' + value: '{ALERT.MESSAGE}' + 13: + name: 'field_string:subject' + value: '{ALERT.SUBJECT}' + 15: + name: priority_average + value: Normal + 16: + name: priority_default + value: Normal + 17: + name: priority_disaster + value: High + 18: + name: priority_high + value: High + 19: + name: priority_information + value: Low + 20: + name: priority_not_classified + value: Low + 21: + name: priority_warning + value: Medium + 8: + name: sd_on_demand_client_id + value: '<PLACE ON DEMAND CLIENT ID>' + 9: + name: sd_on_demand_client_secret + value: '<PLACE ON DEMAND CLIENT SECRET>' + 10: + name: sd_on_demand_refresh_token + value: '<PLACE ON DEMAND REFRESH TOKEN>' + 23: + name: sd_on_demand_url_auth + value: '<PLACE AUTHENTICATION URL FOR ON DEMAND>' + 7: + name: sd_on_premise + value: 'true' + 4: + name: sd_on_premise_auth_token + value: '<PLACE ON PREMISE TECHNICIAN_KEY>' + 12: + name: sd_request_id + value: '{EVENT.TAGS.__zbx_sd_request_id}' + 5: + name: sd_url + value: '<PLACE INSTANCE URL>' + 6: + name: trigger_description + value: '{TRIGGER.DESCRIPTION}' + script: | + var MEngine = { + params: {}, + + setParams: function (params) { + if (typeof params !== 'object') { + return; + } + + MEngine.params = params; + if (typeof MEngine.params.url === 'string') { + if (!MEngine.params.url.endsWith('/')) { + MEngine.params.url += '/'; + } + + MEngine.params.url += 'api/v3/'; + } + + if (MEngine.params.on_premise.toLowerCase() !== 'true' + && typeof MEngine.params.on_demand_url_auth === 'string') { + if (!MEngine.params.on_demand_url_auth.endsWith('/')) { + MEngine.params.on_demand_url_auth += '/'; + } + + MEngine.params.on_demand_url_auth += 'oauth/v2/token?'; + } + }, + + setProxy: function (HTTPProxy) { + MEngine.HTTPProxy = HTTPProxy; + }, + + createLink: function (id, url) { + return url + (url.endsWith('/') ? '' : '/') + + ((MEngine.params.on_premise.toLowerCase() === 'true') + ? ('WorkOrder.do?woMode=viewWO&woID=' + id) + : ('app/itdesk/ui/requests/' + id + '/details') + ); + }, + + refreshAccessToken: function () { + [ + 'on_demand_url_auth', + 'on_demand_refresh_token', + 'on_demand_client_id', + 'on_demand_client_secret' + ].forEach(function (field) { + if (typeof MEngine.params !== 'object' || typeof MEngine.params[field] === 'undefined' + || MEngine.params[field].trim() === '' ) { + throw 'Required MEngine param is not set: "sd_' + field + '".'; + } + }); + + var response, + request = new HttpRequest(), + url = MEngine.params.on_demand_url_auth + + 'refresh_token=' + encodeURIComponent(MEngine.params.on_demand_refresh_token) + + '&grant_type=refresh_token&client_id=' + encodeURIComponent(MEngine.params.on_demand_client_id) + + '&client_secret=' + encodeURIComponent(MEngine.params.on_demand_client_secret) + + '&redirect_uri=https://www.zoho.com&scope=SDPOnDemand.requests.ALL'; + + if (MEngine.HTTPProxy) { + request.setProxy(MEngine.HTTPProxy); + } + + Zabbix.log(4, '[ ManageEngine Webhook ] Refreshing access token. Request: ' + url); + + response = request.post(url); + + Zabbix.log(4, '[ ManageEngine Webhook ] Received response with status code ' + + request.getStatus() + '\n' + response); + + try { + response = JSON.parse(response); + } + catch (error) { + Zabbix.log(4, '[ ManageEngine Webhook ] Failed to parse response received from Zoho Accounts'); + } + + if ((request.getStatus() < 200 || request.getStatus() >= 300) && !response.access_token) { + throw 'Access token refresh failed with HTTP status code ' + request.getStatus() + + '. Check debug log for more information.'; + } + else { + MEngine.params.on_demand_auth_token = response.access_token; + } + }, + + request: function (method, query, data) { + var response, + url = MEngine.params.url + query, + input, + request = new HttpRequest(), + message; + + if (MEngine.params.on_premise.toLowerCase() === 'true') { + request.addHeader('TECHNICIAN_KEY: ' + MEngine.params.on_premise_auth_token); + } + else { + request.addHeader('Authorization: Zoho-oauthtoken ' + MEngine.params.on_demand_auth_token); + request.addHeader('Accept: application/v3+json'); + } + + if (MEngine.HTTPProxy) { + request.setProxy(MEngine.HTTPProxy); + } + + if (typeof data !== 'undefined') { + data = JSON.stringify(data); + } + + input = 'input_data=' + encodeURIComponent(data); + Zabbix.log(4, '[ ManageEngine Webhook ] Sending request: ' + url + '?' + input); + + switch (method) { + case 'post': + response = request.post(url, input); + break; + + case 'put': + response = request.put(url, input); + break; + + default: + throw 'Unsupported HTTP request method: ' + method; + } + + Zabbix.log(4, '[ ManageEngine Webhook ] Received response with status code ' + + request.getStatus() + '\n' + response); + + try { + response = JSON.parse(response); + } + catch (error) { + Zabbix.log(4, '[ ManageEngine Webhook ] Failed to parse response received from ManageEngine'); + } + + if ((request.getStatus() < 200 || request.getStatus() >= 300) + && typeof response.response_status !== 'object') { + throw 'Request failed with HTTP status code ' + request.getStatus() + + '. Check debug log for more information.'; + } + else if (typeof response.response_status === 'object' && response.response_status.status === 'failed') { + message = 'Request failed with status_code '; + + if (typeof response.response_status.messages === 'object' + && response.response_status.messages[0] + && response.response_status.messages[0].message) { + message += response.response_status.messages[0].status_code + + '. Message: ' + response.response_status.messages[0].message; + } + else { + message += response.response_status.status_code; + } + + message += '. Check debug log for more information.'; + throw message; + } + else if (response.request) { + return response.request.id; + } + }, + + createPaylaod: function (fields, isNote) { + var data = {}, + result; + + if (isNote) { + data.description = fields['field_string:description'].replace(/(?:\r\n|\r|\n)/g, '<br>'); + result = {request_note: data}; + } + else { + Object.keys(fields) + .forEach(function(field) { + if (fields[field].trim() === '') { + Zabbix.log(4, '[ ManageEngine Webhook ] Field "' + field + + '" can\'t be empty. The field ignored.'); + } + else { + try { + var prefix = field.split(':')[0], + root; + + if (prefix.startsWith('udf_') && !data.udf_fields) { + data.udf_fields = {}; + root = data.udf_fields; + } + else if (prefix.startsWith('udf_')) { + root = data.udf_fields; + } + else { + root = data; + } + + if (prefix.endsWith('string')) { + root[field.substring(field.indexOf(':') + 1) + .toLowerCase()] = fields[field]; + } + else { + root[field.substring(field.indexOf(':') + 1) + .toLowerCase()] = { + name: fields[field] + }; + } + } + catch (error) { + Zabbix.log(4, '[ ManageEngine Webhook ] Can\'t parse field "' + field + + '". The field ignored.'); + } + } + }); + if (data.description) { + data.description = data.description.replace(/(?:\r\n|\r|\n)/g, '<br>'); + } + + result = {request: data}; + } + + return result; + } + }; + + try { + var params = JSON.parse(value), + fields = {}, + sd = {}, + result = {tags: {}}, + required_params = [ + 'sd_on_premise', 'field_string:subject', 'field_string:description', + 'event_recovery_value', 'event_source', 'event_value', 'event_update_status' + ], + severities = [ + {name: 'not_classified', color: '#97AAB3'}, + {name: 'information', color: '#7499FF'}, + {name: 'warning', color: '#FFC859'}, + {name: 'average', color: '#FFA059'}, + {name: 'high', color: '#E97659'}, + {name: 'disaster', color: '#E45959'}, + {name: 'default', color: '#000000'} + ]; + + Object.keys(params) + .forEach(function (key) { + if (key.startsWith('sd_')) { + sd[key.substring(3)] = params[key]; + } + else if (key.startsWith('field_') || key.startsWith('udf_field_')) { + fields[key] = params[key]; + } + + if (required_params.indexOf(key) !== -1 && params[key].trim() === '') { + throw 'Parameter "' + key + '" can\'t be empty.'; + } + }); + + if ([0, 1, 2, 3].indexOf(parseInt(params.event_source)) === -1) { + throw 'Incorrect "event_source" parameter given: ' + params.event_source + '\nMust be 0-3.'; + } + + // Check {EVENT.VALUE} for trigger-based and internal events. + if (params.event_value !== '0' && params.event_value !== '1' + && (params.event_source === '0' || params.event_source === '3')) { + throw 'Incorrect "event_value" parameter given: ' + params.event_value + '\nMust be 0 or 1.'; + } + + // Check {EVENT.UPDATE.STATUS} only for trigger-based events. + if (params.event_update_status !== '0' && params.event_update_status !== '1' && params.event_source === '0') { + throw 'Incorrect "event_update_status" parameter given: ' + params.event_update_status + '\nMust be 0 or 1.'; + } + + if (params.event_source !== '0' && params.event_recovery_value === '0') { + throw 'Recovery operations are supported only for trigger-based actions.'; + } + + if ([0, 1, 2, 3, 4, 5].indexOf(parseInt(params.event_nseverity)) === -1) { + params.event_nseverity = '6'; + } + + if (params.event_update_status === '1' && (typeof params.sd_request_id === 'undefined' + || params.sd_request_id.trim() === '' + || params.sd_request_id === '{EVENT.TAGS.__zbx_sd_request_id}')) { + throw 'Parameter "sd_request_id" can\'t be empty for update operation.'; + } + + MEngine.setParams(sd); + MEngine.setProxy(params.HTTPProxy); + + if (MEngine.params.on_premise.toLowerCase() !== 'true') { + MEngine.refreshAccessToken(); + } + + // Create issue for non trigger-based events. + if (params.event_source !== '0' && params.event_recovery_value !== '0') { + fields['field_object:priority'] = params['priority_' + severities[params.event_nseverity].name] + || 'Normal'; + + MEngine.request('post', 'requests', MEngine.createPaylaod(fields)); + } + // Create issue for trigger-based events. + else if (params.event_value === '1' && params.event_update_status === '0') { + fields['field_object:priority'] = params['priority_' + severities[params.event_nseverity].name] + || 'Normal'; + + var id = MEngine.request('post', 'requests', MEngine.createPaylaod(fields)); + + result.tags.__zbx_sd_request_id = id; + result.tags.__zbx_sd_request_link = MEngine.createLink(id, params.sd_url); + } + // Update created issue for trigger-based event. + else { + if (params.event_update_status === '1') { + MEngine.request('post', 'requests/' + params.sd_request_id + '/notes', + MEngine.createPaylaod(fields, true) + ); + } + delete fields['field_string:description']; + MEngine.request('put', 'requests/' + params.sd_request_id, MEngine.createPaylaod(fields)); + } + + return JSON.stringify(result); + } + catch (error) { + Zabbix.log(3, '[ ManageEngine Webhook ] ERROR: ' + error); + throw 'Sending failed: ' + error; + } + process_tags: 'YES' + show_event_menu: 'YES' + event_menu_url: '{EVENT.TAGS.__zbx_sd_request_link}' + event_menu_name: 'ManageEngine: {EVENT.TAGS.__zbx_sd_request_id}' + message_templates: + - + event_source: TRIGGERS + operation_mode: PROBLEM + subject: '[{EVENT.STATUS}] {EVENT.NAME}' + message: | + Problem started at {EVENT.TIME} on {EVENT.DATE} + Problem name: {EVENT.NAME} + Host: {HOST.NAME} + Severity: {EVENT.SEVERITY} + Operational data: {EVENT.OPDATA} + Original problem ID: {EVENT.ID} + {TRIGGER.URL} + - + event_source: TRIGGERS + operation_mode: RECOVERY + subject: '[{EVENT.STATUS}] {EVENT.NAME}' + message: | + Problem has been resolved in {EVENT.DURATION} at {EVENT.RECOVERY.TIME} on {EVENT.RECOVERY.DATE} + Problem name: {EVENT.NAME} + Host: {HOST.NAME} + Severity: {EVENT.SEVERITY} + Original problem ID: {EVENT.ID} + {TRIGGER.URL} + - + event_source: TRIGGERS + operation_mode: UPDATE + subject: '[{EVENT.STATUS}] {EVENT.NAME}' + message: | + {USER.FULLNAME} {EVENT.UPDATE.ACTION} problem at {EVENT.UPDATE.DATE} {EVENT.UPDATE.TIME}. + {EVENT.UPDATE.MESSAGE} + + Current problem status is {EVENT.STATUS}, acknowledged: {EVENT.ACK.STATUS}. + - + event_source: DISCOVERY + operation_mode: PROBLEM + subject: 'Discovery: {DISCOVERY.DEVICE.STATUS} {DISCOVERY.DEVICE.IPADDRESS}' + message: | + Discovery rule: {DISCOVERY.RULE.NAME} + + Device IP: {DISCOVERY.DEVICE.IPADDRESS} + Device DNS: {DISCOVERY.DEVICE.DNS} + Device status: {DISCOVERY.DEVICE.STATUS} + Device uptime: {DISCOVERY.DEVICE.UPTIME} + + Device service name: {DISCOVERY.SERVICE.NAME} + Device service port: {DISCOVERY.SERVICE.PORT} + Device service status: {DISCOVERY.SERVICE.STATUS} + Device service uptime: {DISCOVERY.SERVICE.UPTIME} + - + event_source: AUTOREGISTRATION + operation_mode: PROBLEM + subject: 'Autoregistration: {HOST.HOST}' + message: | + Host name: {HOST.HOST} + Host IP: {HOST.IP} + Agent port: {HOST.PORT} |