From 2bcd14006c3a6845ed96753cdbeb8573d8099868 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 19 Nov 2022 22:41:21 +0100 Subject: [PATCH] initial commit --- Dockerfile | 12 +++ app/agent.py | 34 +++++++ app/config/agent.example.yml | 23 +++++ app/config/server.example.yml | 11 ++ .../__pycache__/agent_checker.cpython-310.pyc | Bin 0 -> 2255 bytes .../__pycache__/configuration.cpython-310.pyc | Bin 0 -> 847 bytes app/lib/__pycache__/logger.cpython-310.pyc | Bin 0 -> 274 bytes .../server_checker.cpython-310.pyc | Bin 0 -> 2623 bytes app/lib/agent_checker.py | 95 ++++++++++++++++++ app/lib/configuration.py | 27 +++++ app/lib/logger.py | 7 ++ app/lib/server_checker.py | 88 ++++++++++++++++ app/requirements.txt | 5 + app/server.py | 46 +++++++++ app/static/main.css | 47 +++++++++ app/templates/main.html.j2 | 49 +++++++++ docker-compose.example.yml | 29 ++++++ 17 files changed, 473 insertions(+) create mode 100644 Dockerfile create mode 100755 app/agent.py create mode 100644 app/config/agent.example.yml create mode 100644 app/config/server.example.yml create mode 100644 app/lib/__pycache__/agent_checker.cpython-310.pyc create mode 100644 app/lib/__pycache__/configuration.cpython-310.pyc create mode 100644 app/lib/__pycache__/logger.cpython-310.pyc create mode 100644 app/lib/__pycache__/server_checker.cpython-310.pyc create mode 100644 app/lib/agent_checker.py create mode 100644 app/lib/configuration.py create mode 100644 app/lib/logger.py create mode 100644 app/lib/server_checker.py create mode 100644 app/requirements.txt create mode 100644 app/server.py create mode 100644 app/static/main.css create mode 100644 app/templates/main.html.j2 create mode 100644 docker-compose.example.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e10b89b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3 +WORKDIR /app +ADD ./app/requirements.txt . +RUN pip install -r /app/requirements.txt +RUN apt-get update && \ + apt-get -y install \ + monitoring-plugins \ + monitoring-plugins-contrib && \ + apt-get clean + +ADD ./app . +USER daemon \ No newline at end of file diff --git a/app/agent.py b/app/agent.py new file mode 100755 index 0000000..4442f4d --- /dev/null +++ b/app/agent.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +from flask import Flask, jsonify +from flask_httpauth import HTTPBasicAuth +from werkzeug.security import generate_password_hash, check_password_hash +from lib.logger import logging +from lib.configuration import configuration +from lib.agent_checker import Checker + +config = configuration(prefix='config/agent') +agent = Flask(__name__) +auth = HTTPBasicAuth() +checker = Checker(configuration=config) +log = logging.getLogger('agent') +monitoring_pw_hash = generate_password_hash(config['password']) + + +@auth.verify_password +def verify_password(username: str, password: str): + if username == 'monitoring' and check_password_hash(monitoring_pw_hash, password): + return username + + +@agent.route('/') +@auth.login_required +def index(): + output = { + "check_results": checker.show_data() + } + return jsonify(output) + + +if __name__ == "__main__": + agent.run(host='0.0.0.0', port=5001) diff --git a/app/config/agent.example.yml b/app/config/agent.example.yml new file mode 100644 index 0000000..5329922 --- /dev/null +++ b/app/config/agent.example.yml @@ -0,0 +1,23 @@ +--- +password: test123 + +defaults: + interval: 5 + +checks: + - name: uptime + command: + - /usr/bin/uptime + - name: mpd status + command: + - /usr/bin/systemctl + - status + - mpd + - name: disk check + command: + - /usr/lib/nagios/plugins/check_disk + - "-w" + - "10%" + - "-p" + - "/" + nagios_check: True diff --git a/app/config/server.example.yml b/app/config/server.example.yml new file mode 100644 index 0000000..f92c882 --- /dev/null +++ b/app/config/server.example.yml @@ -0,0 +1,11 @@ +--- +frontend_users: + test: test123 + +defaults: + interval: 30 + password: test123 + +servers: + - name: dev-container + url: http://agent:5001 diff --git a/app/lib/__pycache__/agent_checker.cpython-310.pyc b/app/lib/__pycache__/agent_checker.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb90d8cdca440503ccc8bfd635aa37c86bcce181 GIT binary patch literal 2255 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 Q=dp@W;T7C=!Cd<4KXWrr{{R30 literal 0 HcmV?d00001 diff --git a/app/lib/agent_checker.py b/app/lib/agent_checker.py new file mode 100644 index 0000000..dc17639 --- /dev/null +++ b/app/lib/agent_checker.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +import time +from threading import Timer +from subprocess import run +from lib.logger import logging + +log = logging.getLogger('checker') + + +class Check: + def run_check(self): + self.last_exec_start = time.asctime() + log.debug(f'start command {self.command} at {self.last_exec_start}') + try: + runcheck = run(self.command, capture_output=True) + + # for nagios checks split text and perfdata + perfdata = None + if self.nagios_check: + parts = runcheck.stdout.decode('utf-8').split('|') + output_text = parts[0] + perfdata = parts[1] + else: + output_text = runcheck.stdout.decode('utf-8') + + self.output = { + "rc": runcheck.returncode, + "stdout": runcheck.stdout.decode('utf-8'), + "stderr": runcheck.stderr.decode('utf-8'), + "output_text": output_text + } + if perfdata: + self.output["perfdata"] = perfdata + + if runcheck.returncode == 0: + self.state = "OK" + elif runcheck.returncode == 1: + self.state = "WARNING" + else: + self.state = "CRITICAL" + self.last_exec_finish = time.asctime() + log.debug(f'finished command {self.command} at {self.last_exec_start}') + + except: + log.error(f'error trying to execute {self.command}') + self.state = "CRITICAL" + + self.timer = Timer(interval=self.interval, function=self.run_check) + self.timer.daemon = True + self.timer.start() + + def __init__(self, configuration, check): + defaults = configuration.get('defaults') + self.name = check['name'] + self.command = check['command'] + self.nagios_check = check.get('nagios_check', False) + self.interval = check.get('interval', defaults.get('interval', 300)) + + # pre define variables for check output + self.timer = None + self.state = None + self.output = {} + self.last_exec_finish = None + self.last_exec_start = None + + self.run_check() + + def get_values(self): + values = { + 'name': self.name, + 'command': self.command, + 'last_exec_start': self.last_exec_start, + 'last_exec_finish': self.last_exec_finish, + 'output': self.output, + 'state': self.state + } + return values + + +class Checker: + checks = [] + + def __init__(self, configuration): + for check in configuration['checks']: + log.debug(f"create check {check['name']}") + self.checks.append( + Check( + check=check, + configuration=configuration)) + + def show_data(self): + check_values = [] + for check in self.checks: + check_values.append(check.get_values()) + return check_values diff --git a/app/lib/configuration.py b/app/lib/configuration.py new file mode 100644 index 0000000..2152300 --- /dev/null +++ b/app/lib/configuration.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +from yaml import safe_load +from pathlib import Path +from lib.logger import logging +from sys import exit + +log = logging.getLogger('config') + + +def configuration(prefix: str): + try: + filename = f'{prefix}.yml' + + if not Path(filename).is_file(): + filename = f'{prefix}.example.yml' + log.warning(f'config file not found - using {filename}') + + configfile = open(filename, 'r') + config = safe_load(configfile) + configfile.close() + log.info('configuration loaded successfully') + return config + + except Exception: + log.error(msg='unable to load configuration') + exit(2) + diff --git a/app/lib/logger.py b/app/lib/logger.py new file mode 100644 index 0000000..495692c --- /dev/null +++ b/app/lib/logger.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +import logging + +logging.basicConfig( + format='%(asctime)s %(name)-8s %(levelname)-8s %(message)s', + level=logging.DEBUG) + diff --git a/app/lib/server_checker.py b/app/lib/server_checker.py new file mode 100644 index 0000000..b5e1510 --- /dev/null +++ b/app/lib/server_checker.py @@ -0,0 +1,88 @@ +import time + +from lib.logger import logging +from threading import Timer +from requests import get +log = logging.getLogger('checker') + + +class CheckServer: + def fetch_server(self): + self.last_request_started = time.asctime() + log.debug(f'try to fetch data from {self.name}') + try: + r = get( + self.url, + auth=('monitoring', self.password), + timeout=self.timeout + ) + + if r.status_code == 200: + self.check_results = r.json() + self.server_conn_result = "OK" + elif 400 < r.status_code < 404: + self.server_conn_result = "FORBIDDEN" + elif r.status_code == 404: + self.server_conn_result = "NOT FOUND" + else: + self.server_conn_result = f"Server Error: HTTP {r.status_code}" + self.last_request_finished = time.asctime() + + except ConnectionError: + log.error(f'error connecting to {self.name}') + self.server_conn_result = "UNREACHABLE" + + except: + log.error("something else went wrong") + self.server_conn_result = "UNREACHABLE" + + self.timer = Timer(interval=self.interval, function=self.fetch_server) + self.timer.daemon = True + self.timer.start() + + def __init__(self, server, configuration): + defaults = configuration.get('defaults') + self.url = server['url'] + self.name = server['name'] + self.interval = server.get('interval', defaults.get('interval')) + self.password = server.get('password', defaults.get('password')) + self.timeout = server.get('timeout', defaults.get('timeout', 10)) + + # initialize status variables + self.timer = None + self.last_request_started = None + self.last_request_finished = None + self.check_results = {} + self.server_conn_result = "UNCHECKED" + + self.fetch_server() + + def get_values(self): + values = { + "name": self.name, + "url": self.url, + "server_conn_result": self.server_conn_result, + "last_request_started": self.last_request_started, + "last_request_finished": self.last_request_finished, + "check_results": self.check_results.get('check_results') + } + return values + + +class ServerChecker: + servers = [] + + def __init__(self, configuration): + servers = configuration.get('servers') + for server in servers: + log.debug(f"Monitoring {server.get('name')}") + self.servers.append(CheckServer( + server=server, + configuration=configuration + )) + + def get_data(self): + server_values = [] + for server in self.servers: + server_values.append(server.get_values()) + return server_values diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..f2c8d8d --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,5 @@ +Flask~=2.2.2 +Flask_HTTPAuth~=4.7.0 +Werkzeug~=2.2.2 +PyYAML~=6.0 +Requests~=2.28.1 \ No newline at end of file diff --git a/app/server.py b/app/server.py new file mode 100644 index 0000000..48429c9 --- /dev/null +++ b/app/server.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +import flask +from flask import Flask, jsonify +from flask_httpauth import HTTPBasicAuth +from werkzeug.security import generate_password_hash, check_password_hash +from lib.logger import logging +from lib.configuration import configuration +from lib.server_checker import ServerChecker + +log = logging.getLogger(name='server') +log.info('starting smss server') + +config = configuration(prefix='config/server') +server = Flask(__name__) +auth = HTTPBasicAuth() +users = config.get('frontend_users') +serverchecker = ServerChecker(configuration=config) + + +@auth.verify_password +def verify_password(username, password): + if username in users and check_password_hash(generate_password_hash(users.get(username)), password): + return username + + +@server.route('/') +@auth.login_required +def index(): + output = flask.render_template( + template_name_or_list='main.html.j2', + servers=serverchecker.get_data() + ) + return output + + +@server.route('/json') +@auth.login_required() +def show_json(): + server_data = { + "servers": serverchecker.get_data() + } + return jsonify(server_data) + + +if __name__ == "__main__": + server.run(host="0.0.0.0") diff --git a/app/static/main.css b/app/static/main.css new file mode 100644 index 0000000..6105a1a --- /dev/null +++ b/app/static/main.css @@ -0,0 +1,47 @@ +html { + font-family: sans-serif; + background: #888; +} + +header { + color: #eee; + background: #333; + padding: 20px; +} + +main { + padding: 20px; +} + +main article { + background: #ccc; + padding: 4px; +} + +table.checks { + border-collapse: collapse; +} + +table.checks th { + border: 1px solid #222; +} + +table.checks td { + border: 1px dotted #222; +} + +body { + margin-left: auto; + margin-right: auto; + margin-top: 0px; + background: #eee; + color: #222; +} + +td.status_CRITICAL { + background: #ee2222; +} + +td.status_OK { + background: #22ee22; +} \ No newline at end of file diff --git a/app/templates/main.html.j2 b/app/templates/main.html.j2 new file mode 100644 index 0000000..debd102 --- /dev/null +++ b/app/templates/main.html.j2 @@ -0,0 +1,49 @@ + + + SSMS - Overview + + + +
+

SSMS - servers

+
+
+ {% for server in servers %} +
+

{{ server.get('name') }}

+ url: {{ server.get('url') }} | status: {{ server.server_conn_result }} + + + + + + + + + {% for check in server.get('check_results') %} + + + + + + + + + {% endfor %} +
namestatuscommandoutputlast trylast successful
+ {{ check.get('name') }} + + {{ check.get('state') }} + + {{ check.get('command') | join(' ') }} + + {{ check.get('output').get('output_text') }} + + {{ check.get('last_exec_start') }} + + {{ check.get('last_exec_finish') }} +
+
+ {% endfor %} + + \ No newline at end of file diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..a947b29 --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,29 @@ +--- +version: "3" + +services: + server: + build: . + restart: unless-stopped + stop_signal: SIGKILL + profiles: + - server + - dev + entrypoint: "python /app/server.py" + volumes: + - "./app/config:/app/config:ro" + ports: + - "5000:5000" + + agent: + build: . + restart: unless-stopped + stop_signal: SIGKILL + profiles: + - agent + - dev + entrypoint: "python /app/agent.py" + volumes: + - "./app/config:/app/config:ro" + ports: + - "5001:5001" \ No newline at end of file