initial commit
This commit is contained in:
parent
be416c80ca
commit
2bcd14006c
17 changed files with 473 additions and 0 deletions
12
Dockerfile
Normal file
12
Dockerfile
Normal 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
34
app/agent.py
Executable 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)
|
23
app/config/agent.example.yml
Normal file
23
app/config/agent.example.yml
Normal 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
|
11
app/config/server.example.yml
Normal file
11
app/config/server.example.yml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
frontend_users:
|
||||||
|
test: test123
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
interval: 30
|
||||||
|
password: test123
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- name: dev-container
|
||||||
|
url: http://agent:5001
|
BIN
app/lib/__pycache__/agent_checker.cpython-310.pyc
Normal file
BIN
app/lib/__pycache__/agent_checker.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/lib/__pycache__/configuration.cpython-310.pyc
Normal file
BIN
app/lib/__pycache__/configuration.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/lib/__pycache__/logger.cpython-310.pyc
Normal file
BIN
app/lib/__pycache__/logger.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/lib/__pycache__/server_checker.cpython-310.pyc
Normal file
BIN
app/lib/__pycache__/server_checker.cpython-310.pyc
Normal file
Binary file not shown.
95
app/lib/agent_checker.py
Normal file
95
app/lib/agent_checker.py
Normal 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
27
app/lib/configuration.py
Normal 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
7
app/lib/logger.py
Normal 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
88
app/lib/server_checker.py
Normal 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
5
app/requirements.txt
Normal 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
46
app/server.py
Normal 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
47
app/static/main.css
Normal 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;
|
||||||
|
}
|
49
app/templates/main.html.j2
Normal file
49
app/templates/main.html.j2
Normal 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>
|
29
docker-compose.example.yml
Normal file
29
docker-compose.example.yml
Normal 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"
|
Loading…
Reference in a new issue