initial commit

This commit is contained in:
bsod 2022-11-19 22:41:21 +01:00
parent be416c80ca
commit 2bcd14006c
17 changed files with 473 additions and 0 deletions

12
Dockerfile Normal file
View file

@ -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

34
app/agent.py Executable file
View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,11 @@
---
frontend_users:
test: test123
defaults:
interval: 30
password: test123
servers:
- name: dev-container
url: http://agent:5001

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

95
app/lib/agent_checker.py Normal file
View file

@ -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

27
app/lib/configuration.py Normal file
View file

@ -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)

7
app/lib/logger.py Normal file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env python
import logging
logging.basicConfig(
format='%(asctime)s %(name)-8s %(levelname)-8s %(message)s',
level=logging.DEBUG)

88
app/lib/server_checker.py Normal file
View file

@ -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

5
app/requirements.txt Normal file
View file

@ -0,0 +1,5 @@
Flask~=2.2.2
Flask_HTTPAuth~=4.7.0
Werkzeug~=2.2.2
PyYAML~=6.0
Requests~=2.28.1

46
app/server.py Normal file
View file

@ -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")

47
app/static/main.css Normal file
View file

@ -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;
}

View file

@ -0,0 +1,49 @@
<html>
<head>
<title>SSMS - Overview</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
</head>
<body>
<header>
<h1>SSMS - servers</h1>
</header>
<main>
{% for server in servers %}
<article>
<h1>{{ server.get('name') }}</h1>
url: {{ server.get('url') }} | status: {{ server.server_conn_result }}
<table class="checks">
<tr>
<th>name</th>
<th>status</th>
<th>command</th>
<th>output</th>
<th>last try</th>
<th>last successful</th>
{% for check in server.get('check_results') %}
<tr>
<td>
{{ check.get('name') }}
</td>
<td class="status_{{ check.get('state') }}">
{{ check.get('state') }}
</td>
<td>
{{ check.get('command') | join(' ') }}
</td>
<td>
{{ check.get('output').get('output_text') }}
</td>
<td>
{{ check.get('last_exec_start') }}
</td>
<td>
{{ check.get('last_exec_finish') }}
</td>
</tr>
{% endfor %}
</table>
</article>
{% endfor %}
</body>
</html>

View file

@ -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"