diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3880860 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: master + hooks: + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: double-quote-string-fixer + - id: end-of-file-fixer + - id: fix-encoding-pragma + args: ['--remove'] + - id: requirements-txt-fixer + - id: trailing-whitespace + - repo: https://gitlab.com/pycqa/flake8 + rev: master + hooks: + - id: flake8 + args: ['--max-line-length=120'] + - repo: https://github.com/FalconSocial/pre-commit-python-sorter + rev: master + hooks: + - id: python-import-sorter + args: ['--silent-overwrite'] + - repo: https://github.com/chewse/pre-commit-mirrors-pydocstyle + rev: master + hooks: + - id: pydocstyle + args: ['--config=.pydocstyle', '--match="(?!test_).*\.py"'] + - repo: https://github.com/jumanjihouse/pre-commit-hooks diff --git a/.pydocstyle b/.pydocstyle new file mode 100644 index 0000000..aef2483 --- /dev/null +++ b/.pydocstyle @@ -0,0 +1,2 @@ +[pydocstyle] +ignore = D100,D104,D400,D203,D204,D101,D213,D202 diff --git a/README.md b/README.md index f1cfbca..559c7c4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,58 @@ # notify-by-telegram -Send Nagios notifications to a Telegram channel +Send Nagios notifications to a Telegram channel. + + +## Telegram bot + +This [tutorial](https://takersplace.de/2019/12/19/telegram-notifications-with-nagios/) explains how to create a Telegram bot. You'll need the *chat_id* and *auth_key* for the next section. + +## Installation + +_This guide has been written for [Debian](https://www.debian.org/). Some commands might slightly change depending on your distribution._ + +Clone the repository: +``` +git clone https://github.com/jouir/notify-by-telegram.git +sudo cp -p notify-by-telegram.py /usr/lib/nagios/plugins/notify-by-telegram.py +sudo chmod 755 /usr/lib/nagios/plugins/notify-by-telegram.py +sudo chown root:root /usr/lib/nagios/plugins/notify-by-telegram.py +``` + +Install dependencies using the package manager: +``` +sudo apt install python3-jinja2 python3-requests +``` + +## Configuration + +Copy and update the configuration file example: +``` +sudo cp -p config.json.example /etc/nagios/telegram.json +sudo chmod 640 root:nagios /etc/nagios/telegram.json +``` + +Add Nagios configurations: +``` +sudo cp -p nagios.cfg /etc/nagios4/conf.d/telegram.cfg +``` + +Ensure Nagios reads the configuration file: +``` +grep cfg_dir=/etc/nagios4/conf.d /etc/nagios4/nagios.cfg +``` + +Then reload service: +``` +systemctl reload nagios4 +``` + +## Logs + +Errors logs can be set with the `--logfile` argument. + +Example: +``` +tail -f /var/log/nagios4/telegram.log +``` + +Log level can be raised using `--verbose` or even more with `--debug` arguments. diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..ce9bd59 --- /dev/null +++ b/config.json.example @@ -0,0 +1,4 @@ +{ + "chat_id": 99, + "auth_key": "hash" +} diff --git a/nagios.cfg b/nagios.cfg new file mode 100644 index 0000000..496b3e8 --- /dev/null +++ b/nagios.cfg @@ -0,0 +1,9 @@ +define command { + command_name notify-host-by-telegram + command_line /usr/lib/nagios/plugins/notify-by-telegram.py -c /etc/nagios/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$" +} + +define command { + command_name notify-service-by-telegram + command_line /usr/lib/nagios/plugins/notify-by-telegram.py -c /etc/nagios/telegram.json --logfile /var/log/nagios4/telegram.log service --notification-type "$NOTIFICATIONTYPE$" --service-desc "$SERVICEDESC$" --host-alias "$HOSTALIAS$" --host-address "$HOSTADDRESS$" --service-state "$SERVICESTATE$" --long-date-time "$LONGDATETIME$" --service-output "$SERVICEOUTPUT$" +} diff --git a/notify-by-telegram.py b/notify-by-telegram.py new file mode 100755 index 0000000..c93c68d --- /dev/null +++ b/notify-by-telegram.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +import argparse +import json +import logging +import os + +import requests +from jinja2 import Template + +logger = logging.getLogger(__name__) + + +class InvalidConfigException(Exception): + pass + + +def parse_arguments(): + parser = argparse.ArgumentParser() + # base arguments + parser.add_argument('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, + help='Print more output') + parser.add_argument('-d', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, + default=logging.WARNING, help='Print even more output') + parser.add_argument('-c', '--config', help='Configuration file location', required=True) + parser.add_argument('-o', '--logfile', help='Logging file location') + subparsers = parser.add_subparsers(title='commands', dest='command', help='Nagios notification type') + + # 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) + host_parser.add_argument('--host-output', help='Nagios $HOSTOUTPUT$', required=True) + host_parser.add_argument('--long-date-time', help='Nagios $LONGDATETIME$', required=True) + + # service notifications + service_parser = subparsers.add_parser('service') + service_parser.add_argument('--notification-type', help='Nagios $NOTIFICATIONTYPE$', required=True) + service_parser.add_argument('--service-desc', help='Nagios $SERVICEDESC$', required=True) + service_parser.add_argument('--host-alias', help='Nagios $HOSTALIAS$', required=True) + service_parser.add_argument('--host-address', help='Nagios $HOSTADDRESS$', required=True) + service_parser.add_argument('--service-state', help='Nagios $SERVICESTATE$', required=True) + service_parser.add_argument('--long-date-time', help='Nagios $LONGDATETIME$', required=True) + service_parser.add_argument('--service-output', help='Nagios $SERVICEOUTPUT$', required=True) + + args = parser.parse_args() + return args + + +def read_config(filename=None): + if filename and os.path.isfile(filename): + with open(filename, 'r') as fd: + return json.load(fd) + else: + return {} + + +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') + + +def setup_logging(args): + logging.basicConfig(format='%(levelname)s: %(message)s', level=args.loglevel, filename=args.logfile) + + +def markdown_escape(text): + for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!']: + text = text.replace(special_char, fr'\{special_char}') + return text + + +def generate_host_payload(chat_id, args): + payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2'} + text = r"""\*\*\*\*\* Nagios \*\*\*\*\* +*Notification Type:* {{notification_type}} +*Host:* {{host_name}} +*State:* {{host_state}} +*Address:* {{host_address}} +*Info:* {{host_output}} +*Date/Time:* {{long_date_time}} +""" + template = Template(text) + 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)) + payload['text'] = text + return payload + + +def generate_service_payload(chat_id, args): + payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2'} + text = r"""\*\*\*\*\* Nagios \*\*\*\*\* +*Notification Type:* {{notification_type}} +*Service:* {{service_desc}} +*Host:* {{host_alias}} +*Address:* {{host_address}} +*State:* {{service_state}} +*Date/Time:* {{long_date_time}} +*Additional Info:* +{{service_output}} +""" + template = Template(text) + 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 send_message(auth_key, payload): + r = requests.post(f'https://api.telegram.org/bot{auth_key}/sendMessage', json=payload) + if r.status_code != requests.codes.ok: + description = r.json().get('description') + logger.error(f'{r.status_code}: {description}') + logger.debug(payload) + + +def main(): + args = parse_arguments() + setup_logging(args) + logger.info(f'reading configuration file {args.config}') + config = read_config(args.config) + logger.info('validating configuration') + validate_config(config) + + logger.info('generating payload') + if args.command == 'host': + payload = generate_host_payload(chat_id=config['chat_id'], args=args) + elif args.command == 'service': + payload = generate_service_payload(chat_id=config['chat_id'], args=args) + 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) + + +if __name__ == '__main__': + main()