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 0000000..bb90d8c Binary files /dev/null and b/app/lib/__pycache__/agent_checker.cpython-310.pyc differ diff --git a/app/lib/__pycache__/configuration.cpython-310.pyc b/app/lib/__pycache__/configuration.cpython-310.pyc new file mode 100644 index 0000000..897d8fb Binary files /dev/null and b/app/lib/__pycache__/configuration.cpython-310.pyc differ diff --git a/app/lib/__pycache__/logger.cpython-310.pyc b/app/lib/__pycache__/logger.cpython-310.pyc new file mode 100644 index 0000000..89457ea Binary files /dev/null and b/app/lib/__pycache__/logger.cpython-310.pyc differ diff --git a/app/lib/__pycache__/server_checker.cpython-310.pyc b/app/lib/__pycache__/server_checker.cpython-310.pyc new file mode 100644 index 0000000..1c5dd14 Binary files /dev/null and b/app/lib/__pycache__/server_checker.cpython-310.pyc differ 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