diff --git a/.circleci/config.yml b/.circleci/config.yml index a5a5a23..9cbf48e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,9 +20,12 @@ executors: tag: type: string default: latest + mysql_tag: + type: string + default: '5.7' docker: - image: circleci/ruby:2.7.2-buster - - image: mysql:5.7 + - image: mysql:<< parameters.mysql_tag >> environment: MYSQL_DATABASE: zabbix MYSQL_USER: zabbix @@ -60,6 +63,19 @@ jobs: steps: - integration_test_with_zabbix + integration_test_with_zabbix_60: + executor: + name: zabbix + mysql_tag: '8.0' + tag: ubuntu-6.0-latest + working_directory: ~/repo + environment: + ZABBIX_API: http://localhost:8080/ + ZABBIX_USER: Admin + ZABBIX_DISPATCHER_MEDIA_TYPE: webhook + steps: + - integration_test_with_zabbix + circleci_is_disabled_job: docker: - image: cimg/base:stable @@ -74,6 +90,7 @@ workflows: jobs: - integration_test_with_zabbix_32 - integration_test_with_zabbix_40 + - integration_test_with_zabbix_60 circleci_is_disabled: jobs: - circleci_is_disabled_job diff --git a/CHANGELOG.md b/CHANGELOG.md index 92f54fd..b912f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 1.1.0 + +- Add webhook media_type for Zabbix 6.0 or higher + ## 1.0.0 - Drop python 2.7 support. diff --git a/README.md b/README.md index 198baa7..e8402a6 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,14 @@ This README explains how this integration works, and how to configure it. ![Internal construction of this pack](./images/internal_construction.png) # Requirements +## st2 pack +* Zabbix >=3.0. It has been tested with v3.0, v3.2 and v4.0. -* Zabbix >3.0. It has been tested with v3.0, v3.2 and v4.0. +## script type distpacher +* Zabbix >=3.0. It has been tested with v3.0, v3.2 and v4.0. + +## webhook type dispatcher +* Zabbix >=6.0. It has been tested with v6.0. # Installation Install the pack: @@ -34,6 +40,13 @@ Options: -s Z_SENDTO, --sendto=Z_SENDTO Address, user name or other identifier of the recipient + -s Z_DISPATCHERTYPE, --type=Z_DISPATCHERTYPE + Type of way to dispatch. If you select script, + the Zabbix will use st2_dispatcher. On the + other hand, if you select webhook, this command + will import a webhook dispathcer mediatype which + is defined in media_stackstorm.yml + ``` Example execution: @@ -58,9 +71,12 @@ After executing the `register_st2_config_to_zabbix.py` command, you can notice t You see following page, and you have to fill out with parameters for your st2 environment (the endpoint URLs of st2-api and st2-auth, and authentication information). ![](./images/configuration_for_mediatype2.png) +If you setup webhook dispatcher, you see following page. +![](./images/configuration_for_mediatype3.png) + You can specify additional parameters and you can handle them from the payload of the StackStorm's Trigger(`zabbix.event_handler`). -### Deploy the AlertScript +### Deploy the AlertScript for script dispatcher The script `st2_dispatch.py` sends Zabbix events to the StackStorm server. Copy this script to the directory which Zabbix MediaType refers to. The directory is specified by the parameter of `AlertScriptsPath` in the Zabbix configuration file on the node which zabbix was installed. ```shell @@ -253,6 +269,12 @@ Starting procedure to run the test is also simple, all you have to do is executi $ bundle exec rspec ``` +When you want to run test for webhook type, you have to execute following command. + +``` +$ ZABBIX_DISPATCHER_MEDIA_TYPE=webhook bundle exec rspec +``` + # Advanced Usage If you would prefer to use an API Key for auth in place of user/pass, you can do so by passing a JSON Dict as the first positional argument in your `Media Type` in place of: ``` @@ -277,6 +299,8 @@ This dict has the following valid keys `api_key` will cause `st2_userid` and `st2_passwd` to be ignored (API Key prefered) `trigger` allows you to specify your own trigger on st2 to send messages to. Default is `zabbix.event_handler` +> WARNING: webhook type dispatcher does NOT support user/pass auth, please use API Key. + ### JSON Examples API Key for Auth - `{"api_url":"https://stackstorm.yourdomain.com/api/v1", "api_key":"aaabbbccc111222333"}` User/Pass for auth - `{"api_url":"https://stackstorm.yourdomain.com/api/v1", "auth_url":"https://stackstorm.yourdomain.com/auth", "st2_userid":"st2admin", "st2_passwd":"st2pass"}` diff --git a/images/configuration_for_mediatype3.png b/images/configuration_for_mediatype3.png new file mode 100644 index 0000000..c943c2b Binary files /dev/null and b/images/configuration_for_mediatype3.png differ diff --git a/media_stackstorm.yml b/media_stackstorm.yml new file mode 100644 index 0000000..2b58b50 --- /dev/null +++ b/media_stackstorm.yml @@ -0,0 +1,236 @@ +zabbix_export: + version: '6.0' + media_types: + - + name: StackStorm + type: WEBHOOK + description: StackStorm + parameters: + - + name: alert_message + value: '{ALERT.MESSAGE}' + - + name: alert_subject + value: '{ALERT.SUBJECT}' + - + name: alert_sendto + value: '{ALERT.SENDTO}' + - + name: event_source + value: '{EVENT.SOURCE}' + - + name: st2_api_url + value: '' + - + name: st2_api_key + value: '' + - + name: st2_trigger + value: zabbix.event_handler + script: | + var St2 = { + params: {}, + + setParams: function (params) { + if (typeof params !== 'object') { + return; + } + St2.params = params; + }, + + setProxy: function (HTTPProxy) { + St2.HTTPProxy = HTTPProxy; + }, + + urlCheckFormat: function (api_url) { + if (typeof api_url === 'string' && !api_url.endsWith('/')) { + api_url += '/'; + } + + if (api_url.indexOf('http://') === -1 && api_url.indexOf('https://') === -1) { + api_url = 'https://' + api_url; + } + + return api_url; + }, + + request: function (api_url, data) { + if (typeof St2.params !== 'object' || typeof St2.params['api_key'] === 'undefined' || St2.params['api_key'] === '') { + throw 'Required St2 param is not set: "api_key".'; + } + + var response, + request = new HttpRequest(); + + request.addHeader('Content-Type: application/json'); + request.addHeader('St2-Api-Key: ' + St2.params.api_key); + + const webhook_url = api_url + 'webhooks/st2'; + + if (typeof St2.HTTPProxy !== 'undefined' && St2.HTTPProxy !== '') { + request.setProxy(St2.HTTPProxy); + } + + if (typeof data !== 'undefined') { + data = JSON.stringify(data); + } + + Zabbix.log(4, '[ StackStorm Webhook ] Sending request: ' + webhook_url + ((typeof data === 'string') + ? ('\n' + data) + : '')); + + response = request.post(webhook_url, data); + + Zabbix.log(4, '[ StackStorm Webhook ] Received response with status code ' + + request.getStatus() + '\n' + response); + + if (response !== null) { + try { + response = JSON.parse(response); + } + catch (error) { + Zabbix.log(4, '[ StackStorm Webhook ] Failed to parse response received from StackStorm'); + response = null; + } + } + + if (typeof response !== 'object') { + throw 'Failed to process response received from StackStorm. Check debug log for more information.'; + } + + if (request.getStatus() < 200 || request.getStatus() >= 300) { + var message = 'Request failed with status code ' + request.getStatus(); + + if (response.message) { + message += ': ' + response.message; + } + + throw message + ' Check debug log for more information.'; + } + + return response; + } + }; + + try { + var params = JSON.parse(value), + st2 = {}, + data = {}, + result + required_params = [ + 'alert_subject', 'alert_message', + 'st2_api_url', 'st2_api_key', 'st2_trigger' + ]; + + Object.keys(params) + .forEach(function (key) { + if (key.startsWith('st2_')) { + st2[key.substring(4)] = params[key]; + } + else if (required_params.indexOf(key) !== -1 && params[key] === '') { + throw 'Parameter "' + key + '" can\'t be empty.'; + } + }); + + // Check type of event. Possible values: 0 - Trigger + if (params.event_source != 0) { + throw ('Incorrect "event_source" parameter given: ' + params.event_source + + '\nOnly trigger-based events are supported'); + } + + // Check for backslash in the end of url and schema. + st2.api_url = St2.urlCheckFormat(st2.api_url); + + + data.trigger = st2.trigger; + data.payload = { + alert_sendto: params.alert_sendto, + alert_subject: params.alert_subject, + alert_message: params.alert_message + } + + St2.setParams(st2); + St2.setProxy(params.HTTPProxy); + + var response = St2.request(st2.api_url, data); + + Zabbix.log(4, '[ StackStorm Webhook ] Response: ' + JSON.stringify(response)); + return JSON.stringify(response); + } + catch (error) { + Zabbix.log(4, '[ StackStorm Webhook ] ERROR: ' + error); + throw 'Sending failed: ' + error; + } + message_templates: + - + event_source: TRIGGERS + operation_mode: PROBLEM + subject: '[{TRIGGER.STATUS}] {TRIGGER.NAME}' + message: | + { + "event": { + "id": "{EVENT.ID}", + "time": "{EVENT.TIME}" + }, + "trigger": { + "id": "{TRIGGER.ID}", + "name": "{TRIGGER.NAME}", + "status": "{TRIGGER.STATUS}" + }, + "items": [ + { + "name": "{ITEM.NAME1}", + "host": "{HOST.NAME1}", + "key": "{ITEM.KEY1}", + "value": "{ITEM.VALUE1}" + }, + { + "name": "{ITEM.NAME2}", + "host": "{HOST.NAME2}", + "key": "{ITEM.KEY2}", + "value": "{ITEM.VALUE2}" + }, + { + "name": "{ITEM.NAME3}", + "host": "{HOST.NAME3}", + "key": "{ITEM.KEY3}", + "value": "{ITEM.VALUE3}" + }, + { + "name": "{ITEM.NAME4}", + "host": "{HOST.NAME4}", + "key": "{ITEM.KEY4}", + "value": "{ITEM.VALUE4}" + }, + { + "name": "{ITEM.NAME5}", + "host": "{HOST.NAME5}", + "key": "{ITEM.KEY5}", + "value": "{ITEM.VALUE5}" + }, + { + "name": "{ITEM.NAME6}", + "host": "{HOST.NAME6}", + "key": "{ITEM.KEY6}", + "value": "{ITEM.VALUE6}" + }, + { + "name": "{ITEM.NAME7}", + "host": "{HOST.NAME7}", + "key": "{ITEM.KEY7}", + "value": "{ITEM.VALUE7}" + }, + { + "name": "{ITEM.NAME8}", + "host": "{HOST.NAME8}", + "key": "{ITEM.KEY8}", + "value": "{ITEM.VALUE8}" + }, + { + "name": "{ITEM.NAME9}", + "host": "{HOST.NAME9}", + "key": "{ITEM.KEY9}", + "value": "{ITEM.VALUE9}" + } + ] + } diff --git a/pack.yaml b/pack.yaml index 75deae3..9c66ace 100644 --- a/pack.yaml +++ b/pack.yaml @@ -5,7 +5,7 @@ description: Zabbix Monitoring System keywords: - zabbix - monitoring -version: 1.0.0 +version: 1.1.0 author: Hiroyasu OHYAMA email: user.localhost2000@gmail.com python_versions: diff --git a/requirements.txt b/requirements.txt index cf48552..286dc56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ py-zabbix==1.1.3 st2client pytz tzlocal +pyyaml diff --git a/spec/localhost/tools_register_config_for_st2_spec.rb b/spec/localhost/tools_register_config_for_st2_spec.rb index 28aedec..e00aa9b 100644 --- a/spec/localhost/tools_register_config_for_st2_spec.rb +++ b/spec/localhost/tools_register_config_for_st2_spec.rb @@ -5,6 +5,7 @@ ZABBIX_SENDTO = ENV['ZABBIX_SENDTO'] || 'admin' ZABBIX_PASSWORD = ENV['ZABBIX_PASSWORD'] || 'zabbix' ZABBIX_API_ENDPOINT = ENV['ZABBIX_API'] || 'http://localhost/' +ZABBIX_DISPATCHER_MEDIA_TYPE = ENV['ZABBIX_DISPATCHER_MEDIA_TYPE'] || 'script' describe 'Tests for registering Zabbix for StackStorm' do before(:all) do @@ -18,7 +19,8 @@ "-u #{ ZABBIX_USER } " \ "-s #{ ZABBIX_SENDTO } " \ "-p #{ ZABBIX_PASSWORD } " \ - "-z #{ ZABBIX_API_ENDPOINT }") do + "-z #{ ZABBIX_API_ENDPOINT } " \ + "-t #{ ZABBIX_DISPATCHER_MEDIA_TYPE }") do its(:exit_status) { should eq 0 } its(:stdout) do should match /^Success to register the configurations for StackStorm to the Zabbix Server./ @@ -38,7 +40,7 @@ # This method wait to start and initialize Zabbix-server and Zabbix-Web def try_to_login(retry_count = 0) begin - return @client.login('admin', 'zabbix') + return @client.login(ZABBIX_USER, ZABBIX_PASSWORD) rescue => e if retry_count < 60 # make a delay before retrying diff --git a/tests/test_tool_register_st2_config_to_zabbix.py b/tests/test_tool_register_st2_config_to_zabbix.py index 82ca5ac..ffe54ce 100644 --- a/tests/test_tool_register_st2_config_to_zabbix.py +++ b/tests/test_tool_register_st2_config_to_zabbix.py @@ -163,3 +163,8 @@ def side_effect_userupdate(*args, **kwargs): mock_client.apiinfo.version.return_value = '4.x.y' ret = register_st2_config_to_zabbix.register_media_to_admin(mock_client, 1, mock.Mock()) self.assertEqual(ret, 'user.update is called') + + # When sending request that changes MediaType to Zabbix6.x, this calls user.update API + mock_client.apiinfo.version.return_value = '6.x.y' + ret = register_st2_config_to_zabbix.register_media_to_admin(mock_client, 1, mock.Mock()) + self.assertEqual(ret, 'user.update is called') diff --git a/tools/register_st2_config_to_zabbix.py b/tools/register_st2_config_to_zabbix.py index b401c98..2c6fbb0 100755 --- a/tools/register_st2_config_to_zabbix.py +++ b/tools/register_st2_config_to_zabbix.py @@ -6,6 +6,7 @@ from zabbix.api import ZabbixAPI from pyzabbix.api import ZabbixAPIException from six.moves.urllib.error import URLError +import yaml # This constant describes 'script' value of 'type' property in the MediaType, # which is specified in the Zabbix API specification. @@ -27,6 +28,9 @@ def get_options(): help="Password which is associated with the username") parser.add_option('-s', '--sendto', dest="z_sendto", default='Admin', help="Address, user name or other identifier of the recipient") + parser.add_option('-t', '--type', dest="z_dispatchertype", + default='script', choices=['script', 'webhook'], + help="Select type of way to dispatch") (options, args) = parser.parse_args() @@ -58,73 +62,127 @@ def register_media_type(client, options, mediatype_id=None): """ This method registers a MediaType which dispatches alert to the StackStorm. """ - mediatype_args = [ - '-- CHANGE ME : api_url (e.g. https://st2-node/api/v1)', - '-- CHANGE ME : auth_url (e.g. https://st2-node/auth/v1)', - '-- CHANGE ME : login uername of StackStorm --', - '-- CHANGE ME : login password of StackStorm --', - '{ALERT.SENDTO}', - '{ALERT.SUBJECT}', - '{ALERT.MESSAGE}', - ] - - # send request to register a new MediaType for StackStorm - params = { - 'description': 'StackStorm', - 'type': SCRIPT_MEDIA_TYPE, - 'exec_path': ST2_DISPATCHER_SCRIPT, - 'exec_params': "\n".join(mediatype_args) + "\n", - } - if mediatype_id: - params['mediatypeid'] = mediatype_id - - ret = client.mediatype.update(**params) + if (options.z_dispatchertype == 'script'): + mediatype_args = [ + '-- CHANGE ME : api_url (e.g. https://st2-node/api/v1)', + '-- CHANGE ME : auth_url (e.g. https://st2-node/auth/v1)', + '-- CHANGE ME : login uername of StackStorm --', + '-- CHANGE ME : login password of StackStorm --', + '{ALERT.SENDTO}', + '{ALERT.SUBJECT}', + '{ALERT.MESSAGE}', + ] + + # send request to register a new MediaType for StackStorm + params = { + 'description': 'StackStorm', + 'type': SCRIPT_MEDIA_TYPE, + 'exec_path': ST2_DISPATCHER_SCRIPT, + 'exec_params': "\n".join(mediatype_args) + "\n", + } + if mediatype_id: + params['mediatypeid'] = mediatype_id + + ret = client.mediatype.update(**params) + else: + ret = client.mediatype.create(**params) + + return_value = ret['mediatypeids'][0] else: - ret = client.mediatype.create(**params) + with open('media_stackstorm.yml') as file: + media_stackstorm = yaml.safe_load(file) + params = { + 'format': 'yaml', + 'source': yaml.dump(media_stackstorm), + 'rules': { + 'mediaTypes': { + 'createMissing': True, + 'updateExisting': True + } + } + } + + ret = client.do_request('configuration.import', params) - return ret['mediatypeids'][0] + if (not ret['result']): + sys.exit('Failed to import StackStorm media_type') + + params = { + 'output': 'yaml', + 'filter': { + 'name': media_stackstorm['zabbix_export']['media_types'][0]['name'] + } + } + + ret = client.mediatype.get(**params) + return_value = ret[0]['mediatypeid'] + + return return_value def register_action(client, mediatype_id, options, action_id=None): + ret = None if action_id: client.action.delete(action_id) - return client.action.create(**{ - 'name': ST2_ACTION_NAME, - 'esc_period': 360, - 'eventsource': 0, # means event created by a trigger - 'def_shortdata': '{TRIGGER.STATUS}: {TRIGGER.NAME}', - 'def_longdata': json.dumps({ - 'event': { - 'id': '{EVENT.ID}', - 'time': '{EVENT.TIME}', - }, - 'trigger': { - 'id': '{TRIGGER.ID}', - 'name': '{TRIGGER.NAME}', - 'status': '{TRIGGER.STATUS}', - }, - 'items': [{ - 'name': '{ITEM.NAME%s}' % index, - 'host': '{HOST.NAME%s}' % index, - 'key': '{ITEM.KEY%s}' % index, - 'value': '{ITEM.VALUE%s}' % index - } for index in range(1, 9)], - }), - 'operations': [{ - "operationtype": 0, - "esc_period": 0, - "esc_step_from": 1, - "esc_step_to": 1, - "evaltype": 0, - "opmessage_usr": [{"userid": "1"}], - "opmessage": { - "default_msg": 1, - "mediatypeid": mediatype_id, - } - }] - }) + major_version = int(client.apiinfo.version()[0]) + if (major_version <= 4): + ret = client.action.create(**{ + 'name': ST2_ACTION_NAME, + 'esc_period': 360, + 'eventsource': 0, # means event created by a trigger + 'def_shortdata': '{TRIGGER.STATUS}: {TRIGGER.NAME}', + 'def_longdata': json.dumps({ + 'event': { + 'id': '{EVENT.ID}', + 'time': '{EVENT.TIME}', + }, + 'trigger': { + 'id': '{TRIGGER.ID}', + 'name': '{TRIGGER.NAME}', + 'status': '{TRIGGER.STATUS}', + }, + 'items': [{ + 'name': '{ITEM.NAME%s}' % index, + 'host': '{HOST.NAME%s}' % index, + 'key': '{ITEM.KEY%s}' % index, + 'value': '{ITEM.VALUE%s}' % index + } for index in range(1, 9)], + }), + 'operations': [{ + "operationtype": 0, + "esc_period": 0, + "esc_step_from": 1, + "esc_step_to": 1, + "evaltype": 0, + "opmessage_usr": [{"userid": "1"}], + "opmessage": { + "default_msg": 1, + "mediatypeid": mediatype_id, + } + }] + }) + + if (5 <= major_version): + ret = client.action.create(**{ + 'name': ST2_ACTION_NAME, + 'esc_period': 360, + 'eventsource': 0, # means event created by a trigger + 'operations': [{ + "operationtype": 0, + "esc_period": 0, + "esc_step_from": 1, + "esc_step_to": 1, + "evaltype": 0, + "opmessage_usr": [{"userid": "1"}], + "opmessage": { + "default_msg": 1, + "mediatypeid": mediatype_id, + } + }] + }) + return ret def register_media_to_admin(client, mediatype_id, options):