From a0507d546a49eee5c5daa8c722545800b5af768d Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 26 Nov 2020 13:02:08 +0100 Subject: [PATCH 01/10] Update typo in README Signed-off-by: Julien Riou --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e799bf6..9d6cad2 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ Copy and update the configuration file example: cp -p config.json.example telegram.json vim telegram.json sudo mv telegram.json /etc/nagios4/telegram.json -sudo chmod 640 root:nagios /etc/nagios4/telegram.json +sudo chown root:nagios /etc/nagios4/telegram.json +sudo chmod 640 /etc/nagios4/telegram.json ``` Ensure Nagios reads the configuration file: From a7c9f1455eb29f50fca1392d6abc1f0102825351 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 26 Nov 2020 13:18:15 +0100 Subject: [PATCH 02/10] Factorize payload generation Signed-off-by: Julien Riou --- TODO.txt | 1 - notify-by-telegram.py | 73 ++++++++++++++++++++++++++----------------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/TODO.txt b/TODO.txt index 12a5bc7..5d7e48a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,4 +1,3 @@ -Factorize the template rendering functions Use JSON schema to validate configuration file Add pre-commit Dockerfile and a doc to easily lint the code Add requirements.txt file diff --git a/notify-by-telegram.py b/notify-by-telegram.py index 2b5ebe6..916b4fc 100755 --- a/notify-by-telegram.py +++ b/notify-by-telegram.py @@ -76,43 +76,52 @@ def markdown_escape(text): return text -def generate_host_payload(chat_id, args, template_file_name=None): +def generate_payload(chat_id, message_type, message_variables, template_file_name=None): payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2'} + + # define jinja template name if not template_file_name: + template_name = 'host.md.j2' if message_type == 'host' else 'service.md.j2' absolute_path = os.path.split(os.path.abspath(__file__))[0] - template_file_name = os.path.join(absolute_path, 'templates', 'host.md.j2') + template_file_name = os.path.join(absolute_path, 'templates', template_name) template_path = os.path.dirname(os.path.abspath(template_file_name)) template_name = os.path.basename(os.path.abspath(template_file_name)) + + # create a jinja file template loader = FileSystemLoader(template_path) env = Environment(loader=loader) template = env.get_template(template_name) - text = template.render(notification_type=markdown_escape(args.notification_type), - host_name=markdown_escape(args.host_name), host_state=markdown_escape(args.host_state), - host_address=markdown_escape(args.host_address), - host_output=markdown_escape(args.host_output), - long_date_time=markdown_escape(args.long_date_time)) + + # escape message variables + template_variables = {} + for key, value in message_variables.items(): + template_variables[key] = markdown_escape(value) + + # render template and update payload + text = template.render(**template_variables) payload['text'] = text return payload -def generate_service_payload(chat_id, args, template_file_name=None): - payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2'} - if not template_file_name: - absolute_path = os.path.split(os.path.abspath(__file__))[0] - template_file_name = os.path.join(absolute_path, 'templates', 'service.md.j2') - template_path = os.path.dirname(os.path.abspath(template_file_name)) - template_name = os.path.basename(os.path.abspath(template_file_name)) - loader = FileSystemLoader(template_path) - env = Environment(loader=loader) - template = env.get_template(template_name) - text = template.render(notification_type=markdown_escape(args.notification_type), - service_desc=markdown_escape(args.service_desc), host_alias=markdown_escape(args.host_alias), - host_address=markdown_escape(args.host_address), - service_state=markdown_escape(args.service_state), - long_date_time=markdown_escape(args.long_date_time), - service_output=markdown_escape(args.service_output)) - payload['text'] = text - return payload +def generate_host_payload(chat_id, notification_type, host_name, host_state, host_address, host_output, long_date_time, + template_file_name=None): + message_variables = { + 'notification_type': notification_type, 'host_name': host_name, 'host_state': host_state, + 'host_address': host_address, 'host_output': host_output, 'long_date_time': long_date_time + } + return generate_payload(chat_id=chat_id, message_type='host', message_variables=message_variables, + template_file_name=template_file_name) + + +def generate_service_payload(chat_id, notification_type, service_desc, host_alias, host_address, service_state, + long_date_time, service_output, template_file_name=None): + message_variables = { + 'notification_type': notification_type, 'service_desc': service_desc, 'host_alias': host_alias, + 'host_address': host_address, 'service_state': service_state, 'long_date_time': long_date_time, + 'service_output': service_output + } + return generate_payload(chat_id=chat_id, message_type='service', message_variables=message_variables, + template_file_name=template_file_name) def send_message(auth_key, payload): @@ -134,17 +143,23 @@ def main(): logger.info('generating payload') if args.command == 'host': - payload = generate_host_payload(chat_id=config['chat_id'], args=args, + payload = generate_host_payload(chat_id=config['chat_id'], notification_type=args.notification_type, + host_name=args.host_name, host_state=args.host_state, + host_address=args.host_address, host_output=args.host_output, + long_date_time=args.long_date_time, template_file_name=config.get('host_template')) elif args.command == 'service': - payload = generate_service_payload(chat_id=config['chat_id'], args=args, - template_file_name=config.get('service_template')) + payload = generate_service_payload(chat_id=config['chat_id'], notification_type=args.notification_type, + service_desc=args.service_desc, host_alias=args.host_alias, + host_address=args.host_address, service_state=args.service_state, + long_date_time=args.long_date_time, service_output=args.service_output, + template_file_name=config.get('service_template')) else: raise NotImplementedError(f'command {args.command} not supported') logger.info('sending message to telegram api') send_message(auth_key=config['auth_key'], payload=payload) - except Exception as err: + except Exception: logger.exception('cannot execute program') From 17f0948aebbbbf655ad5077772db02a2aef64298 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 26 Nov 2020 14:48:46 +0100 Subject: [PATCH 03/10] Add configuration validation with jsonschema Signed-off-by: Julien Riou --- .pre-commit-config.yaml | 2 ++ README.md | 4 ++-- TODO.txt | 1 - config.json.example => config.example.json | 0 config.schema.json | 21 +++++++++++++++++++++ notify-by-telegram.py | 9 +++++---- 6 files changed, 30 insertions(+), 7 deletions(-) rename config.json.example => config.example.json (100%) create mode 100644 config.schema.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 503fbbb..a14542e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,8 @@ repos: args: ['--remove'] - id: requirements-txt-fixer - id: trailing-whitespace + - id: check-json + - id: check-yaml - repo: https://gitlab.com/pycqa/flake8 rev: master hooks: diff --git a/README.md b/README.md index 9d6cad2..56220aa 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,14 @@ git clone https://github.com/jouir/notify-by-telegram.git /opt/notify-by-telegra Install dependencies using the package manager: ``` -sudo apt install python3-jinja2 python3-requests +sudo apt install python3-jinja2 python3-requests python3-jsonschema ``` ## Configuration Copy and update the configuration file example: ``` -cp -p config.json.example telegram.json +cp -p config.example.json telegram.json vim telegram.json sudo mv telegram.json /etc/nagios4/telegram.json sudo chown root:nagios /etc/nagios4/telegram.json diff --git a/TODO.txt b/TODO.txt index 5d7e48a..da36fd8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,2 @@ -Use JSON schema to validate configuration file Add pre-commit Dockerfile and a doc to easily lint the code Add requirements.txt file diff --git a/config.json.example b/config.example.json similarity index 100% rename from config.json.example rename to config.example.json diff --git a/config.schema.json b/config.schema.json new file mode 100644 index 0000000..f817acd --- /dev/null +++ b/config.schema.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "properties": { + "chat_id": { + "type": "number" + }, + "auth_key": { + "type": "string" + }, + "host_template": { + "type": "string" + }, + "service_template": { + "type": "string" + } + }, + "required": [ + "chat_id", + "auth_key" + ] +} diff --git a/notify-by-telegram.py b/notify-by-telegram.py index 916b4fc..aac48ad 100755 --- a/notify-by-telegram.py +++ b/notify-by-telegram.py @@ -6,8 +6,10 @@ import os import requests from jinja2 import Environment, FileSystemLoader +from jsonschema import validate logger = logging.getLogger(__name__) +absolute_path = os.path.split(os.path.abspath(__file__))[0] class InvalidConfigException(Exception): @@ -60,9 +62,9 @@ def read_config(filename=None): def validate_config(config): if config is None: raise InvalidConfigException('config is not a dict') - for key in ['chat_id', 'auth_key']: - if key not in config: - raise InvalidConfigException(f'missing "{key}" key in config') + with open(os.path.join(absolute_path, 'config.schema.json'), 'r') as fd: + schema = json.loads(fd.read()) + validate(instance=config, schema=schema) def setup_logging(args): @@ -82,7 +84,6 @@ def generate_payload(chat_id, message_type, message_variables, template_file_nam # define jinja template name if not template_file_name: template_name = 'host.md.j2' if message_type == 'host' else 'service.md.j2' - absolute_path = os.path.split(os.path.abspath(__file__))[0] template_file_name = os.path.join(absolute_path, 'templates', template_name) template_path = os.path.dirname(os.path.abspath(template_file_name)) template_name = os.path.basename(os.path.abspath(template_file_name)) From 2b5a263d486926c235fe545e5ca44d0c2aec9d11 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 26 Nov 2020 14:58:40 +0100 Subject: [PATCH 04/10] Add configuration file to README Signed-off-by: Julien Riou --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 56220aa..add12e2 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,15 @@ Then reload service: systemctl reload nagios4 ``` +## Configuration file + +Format used is JSON with the following keys: + +* `chat_id`: where to send message on Telegram +* `auth_key`: key used to authenticate on Telegram +* `host_template` (optional): path to Markdown template file used for sending host notifications +* `service_template` (optional): path to Markdown template file used for sending service notifications + ## Logs Errors logs can be set with the `--logfile` argument. From 7fa647c95391edff0b3f1c8503dad1e7b4625a04 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 26 Nov 2020 15:06:25 +0100 Subject: [PATCH 05/10] Update README with template variables Signed-off-by: Julien Riou --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index add12e2..bc78816 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Format used is JSON with the following keys: * `host_template` (optional): path to Markdown template file used for sending host notifications * `service_template` (optional): path to Markdown template file used for sending service notifications -## Logs +## Logging Errors logs can be set with the `--logfile` argument. @@ -80,3 +80,24 @@ They can be overriden in the configuration file: ``` Both options are optional. + +### Host variables + +Variables replaced in the host template: +* `notification_type` (= `$NOTIFICATIONTYPE$`) +* `host_name` (= `$HOSTNAME$`) +* `host_state` (= `$HOSTSTATE$`) +* `host_address` (= `$HOSTADDRESS$`) +* `host_output` (= `$HOSTOUTPUT$`) +* `long_date_time` (= `$LONGDATETIME$`) + +### Service variables + +Variables replaced in the service template: +* `notification_type` (= `$NOTIFICATIONTYPE$`) +* `service_desc` (= `$SERVICEDESC$`) +* `host_alias` (= `$HOSTALIAS$`) +* `host_address` (= `$HOSTADDRESS$`) +* `service_state` (= `$SERVICESTATE$`) +* `long_date_time` (= `$LONGDATETIME$`) +* `service_output` (= `$SERVICEOUTPUT$`) From ca3adb00ceeb9382e7bf5636f42a40eef4bc1f05 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 26 Nov 2020 15:23:52 +0100 Subject: [PATCH 06/10] Add a contribution/lint guide Signed-off-by: Julien Riou --- Dockerfile | 5 +++++ README.md | 13 +++++++++++++ TODO.txt | 1 - 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3629d9a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.8-slim +RUN apt-get update \ + && apt-get install -y git \ + && apt-get autoclean \ + && pip install pre-commit diff --git a/README.md b/README.md index bc78816..11ae720 100644 --- a/README.md +++ b/README.md @@ -101,3 +101,16 @@ Variables replaced in the service template: * `service_state` (= `$SERVICESTATE$`) * `long_date_time` (= `$LONGDATETIME$`) * `service_output` (= `$SERVICEOUTPUT$`) + +## How to contribute + +Contributions are welcomed! Feel free to update the code and create a pull-request. + +Be sure to lint the code before: +``` +docker build -t pre-commit . +docker run -it -v $(pwd):/mnt/ --rm pre-commit bash +# cd /mnt/ +# pre-commit run --all-files +# exit +``` diff --git a/TODO.txt b/TODO.txt index da36fd8..06b3974 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,2 +1 @@ -Add pre-commit Dockerfile and a doc to easily lint the code Add requirements.txt file From 7cdc46954e1b462799b539876d5b4b8afd380906 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 26 Nov 2020 15:30:49 +0100 Subject: [PATCH 07/10] Add requirements.txt file Signed-off-by: Julien Riou --- README.md | 7 ++++++- TODO.txt | 1 - requirements.txt | 21 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) delete mode 100644 TODO.txt create mode 100644 requirements.txt diff --git a/README.md b/README.md index 11ae720..b1b2c6b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,12 @@ Clone the repository: git clone https://github.com/jouir/notify-by-telegram.git /opt/notify-by-telegram ``` -Install dependencies using the package manager: +Install dependencies using pip: +``` +pip install -r requirements.txt +``` + +Or via the package manager: ``` sudo apt install python3-jinja2 python3-requests python3-jsonschema ``` diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index 06b3974..0000000 --- a/TODO.txt +++ /dev/null @@ -1 +0,0 @@ -Add requirements.txt file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..973fadd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +appdirs==1.4.4 +attrs==20.3.0 +certifi==2020.11.8 +cfgv==3.2.0 +chardet==3.0.4 +distlib==0.3.1 +filelock==3.0.12 +identify==1.5.10 +idna==2.10 +Jinja2==2.11.2 +jsonschema==3.2.0 +MarkupSafe==1.1.1 +nodeenv==1.5.0 +pre-commit==2.9.2 +pyrsistent==0.17.3 +PyYAML==5.3.1 +requests==2.25.0 +six==1.15.0 +toml==0.10.2 +urllib3==1.26.2 +virtualenv==20.2.1 From 0cd101954cc6b39fd67226827e845715609d5636 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 26 Nov 2020 15:35:59 +0100 Subject: [PATCH 08/10] Remove --service-desc argument for hosts Signed-off-by: Julien Riou --- nagios.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nagios.cfg b/nagios.cfg index 2b0ee46..bdddcff 100644 --- a/nagios.cfg +++ b/nagios.cfg @@ -1,6 +1,6 @@ define command { command_name notify-host-by-telegram - command_line /opt/notify-by-telegram/notify-by-telegram.py -c /etc/nagios4/telegram.json --logfile /var/log/nagios4/telegram.log host --notification-type "$NOTIFICATIONTYPE$" --service-desc "$SERVICEDESC$" --host-name "$HOSTNAME$" --host-state "$HOSTSTATE$" --host-address "$HOSTADDRESS$" --host-output "$HOSTOUTPUT$" --long-date-time "$LONGDATETIME$" + command_line /opt/notify-by-telegram/notify-by-telegram.py -c /etc/nagios4/telegram.json --logfile /var/log/nagios4/telegram.log host --notification-type "$NOTIFICATIONTYPE$" --host-name "$HOSTNAME$" --host-state "$HOSTSTATE$" --host-address "$HOSTADDRESS$" --host-output "$HOSTOUTPUT$" --long-date-time "$LONGDATETIME$" } define command { From df5296af42b79bf4d831a012ff6452fded1ac1ab Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Thu, 25 Mar 2021 10:39:11 +0100 Subject: [PATCH 09/10] Escape '=' to produce valid markdown (#1) Signed-off-by: Julien Riou --- notify-by-telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify-by-telegram.py b/notify-by-telegram.py index aac48ad..9d11874 100755 --- a/notify-by-telegram.py +++ b/notify-by-telegram.py @@ -73,7 +73,7 @@ def setup_logging(args): def markdown_escape(text): - for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!']: + for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!', '=']: text = text.replace(special_char, fr'\{special_char}') return text From 4f854bca8b79a9227408a964c28ef4b97e4bf1dc Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Mon, 9 May 2022 09:57:37 +0200 Subject: [PATCH 10/10] fix: Remove $SERVICEDESC$ from host notification Signed-off-by: Julien Riou --- notify-by-telegram.py | 1 - 1 file changed, 1 deletion(-) diff --git a/notify-by-telegram.py b/notify-by-telegram.py index 9d11874..9333308 100755 --- a/notify-by-telegram.py +++ b/notify-by-telegram.py @@ -30,7 +30,6 @@ def parse_arguments(): # host notifications host_parser = subparsers.add_parser('host') host_parser.add_argument('--notification-type', help='nagios $NOTIFICATIONTYPE$', required=True) - host_parser.add_argument('--service-desc', help='nagios $SERVICEDESC$', required=True) host_parser.add_argument('--host-name', help='nagios $HOSTNAME$', required=True) host_parser.add_argument('--host-state', help='nagios $HOSTSTATE$', required=True) host_parser.add_argument('--host-address', help='nagios $HOSTADDRESS$', required=True)