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/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 e799bf6..b1b2c6b 100644 --- a/README.md +++ b/README.md @@ -15,19 +15,25 @@ 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: ``` -sudo apt install python3-jinja2 python3-requests +pip install -r requirements.txt +``` + +Or via the package manager: +``` +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 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: @@ -40,7 +46,16 @@ Then reload service: systemctl reload nagios4 ``` -## Logs +## 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 + +## Logging Errors logs can be set with the `--logfile` argument. @@ -70,3 +85,37 @@ 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$`) + +## 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 deleted file mode 100644 index 12a5bc7..0000000 --- a/TODO.txt +++ /dev/null @@ -1,4 +0,0 @@ -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/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/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 { diff --git a/notify-by-telegram.py b/notify-by-telegram.py index 2b5ebe6..9333308 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): @@ -28,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) @@ -60,9 +61,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): @@ -71,48 +72,56 @@ 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 -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: - absolute_path = os.path.split(os.path.abspath(__file__))[0] - template_file_name = os.path.join(absolute_path, 'templates', 'host.md.j2') + template_name = 'host.md.j2' if message_type == 'host' else 'service.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') 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