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
+
+
+
+
+
+ {% for server in servers %}
+
+ {{ server.get('name') }}
+ url: {{ server.get('url') }} | status: {{ server.server_conn_result }}
+
+
+ name |
+ status |
+ command |
+ output |
+ last try |
+ last successful |
+ {% for check in server.get('check_results') %}
+
+
+ {{ 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 %}
+
+
+ {% 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