mirror of
https://github.com/FAUSheppy/icinga-webhook-gateway
synced 2026-04-26 22:22:30 +02:00
Compare commits
14 Commits
52569c7687
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b0752c2984 | |||
| 1af07b90cc | |||
| ed039833c5 | |||
| bb5aaa47ad | |||
| 87b8de01d7 | |||
| ef8e1e6a81 | |||
| 6680f4769c | |||
| 95c3551a5c | |||
| 31db0c22d2 | |||
| ce5328da53 | |||
| 1d36a9aaed | |||
| 7fea3bf315 | |||
| 2e37ddcb8e | |||
| 7d612c0ccd |
@@ -1,5 +1,15 @@
|
||||
import icinga2api
|
||||
import icinga2api.client
|
||||
from urllib.parse import urlparse
|
||||
|
||||
def split_url(url):
|
||||
parsed = urlparse(url)
|
||||
|
||||
http_vhost = parsed.hostname
|
||||
http_uri = parsed.path or "/"
|
||||
http_ssl = parsed.scheme == "https"
|
||||
|
||||
return http_vhost, http_uri, http_ssl
|
||||
|
||||
def _create_client(app):
|
||||
|
||||
@@ -39,7 +49,7 @@ def _build_service_name(user, async_service_name):
|
||||
|
||||
return "{}_async_{}".format(user, async_service_name)
|
||||
|
||||
def create_service(user, async_service_name, app):
|
||||
def create_service(user, async_service_name, app, webcheck=False):
|
||||
|
||||
if not app.config.get("ICINGA_API_URL"):
|
||||
return
|
||||
@@ -48,20 +58,43 @@ def create_service(user, async_service_name, app):
|
||||
name = _build_service_name(user, async_service_name)
|
||||
host_name = app.config["ASYNC_ICINGA_DUMMY_HOST"]
|
||||
|
||||
service_config = {
|
||||
"templates": ["generic-service"],
|
||||
"attrs": {
|
||||
"display_name": name,
|
||||
"check_command": "gateway",
|
||||
"host_name" : host_name,
|
||||
"vars" : {
|
||||
"host" : "async-icinga.atlantishq.de",
|
||||
"service_name" : async_service_name,
|
||||
"protocol" : "https",
|
||||
"owner" : user
|
||||
# TODO: query service from DB
|
||||
accepted_return_codes = [200, 204]
|
||||
|
||||
|
||||
if webcheck:
|
||||
http_vhost, http_uri, http_ssl = split_url(url)
|
||||
service_config = {
|
||||
"templates": ["generic-service"],
|
||||
"attrs": {
|
||||
"display_name": name,
|
||||
"check_command": "http",
|
||||
"host_name": host_name,
|
||||
"vars": {
|
||||
"http_vhost": http_vhost,
|
||||
"http_uri": http_uri,
|
||||
"http_expect": http_expect,
|
||||
"http_accept_status": accepted_return_codes, # array
|
||||
"http_ssl": True,
|
||||
"http_sni": True
|
||||
}
|
||||
}
|
||||
}
|
||||
else:
|
||||
service_config = {
|
||||
"templates": ["generic-service"],
|
||||
"attrs": {
|
||||
"display_name": name,
|
||||
"check_command": "gateway",
|
||||
"host_name" : host_name,
|
||||
"vars" : {
|
||||
"host" : "async-icinga.atlantishq.de",
|
||||
"service_name" : async_service_name,
|
||||
"protocol" : "https",
|
||||
"owner" : user
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Create the service (name is required in this format)
|
||||
service_api_helper_name = "{}!{}".format(host_name, name)
|
||||
|
||||
108
server.py
108
server.py
@@ -9,6 +9,7 @@ import datetime
|
||||
import pytimeparse.timeparse as timeparse
|
||||
import sys
|
||||
import secrets
|
||||
import zoneinfo
|
||||
|
||||
import flask_wtf
|
||||
from flask_wtf import FlaskForm
|
||||
@@ -22,6 +23,8 @@ from sqlalchemy.sql import func
|
||||
import sqlalchemy
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy.sql.expression import func
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from psycopg2.errors import UniqueViolation
|
||||
|
||||
import icingatools
|
||||
import smarttools
|
||||
@@ -32,8 +35,25 @@ app = flask.Flask("Icinga Report In Gateway")
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI') or 'sqlite:///database.sqlite'
|
||||
app.config['JSON_CONFIG_FILE'] = 'services.json'
|
||||
app.config['JSON_CONFIG_DIR'] = 'config'
|
||||
app.config['TIME_ZONE'] = zoneinfo.ZoneInfo(os.getenv("TIME_ZONE", "UTC"))
|
||||
app.config['AUTH_HEADER'] = os.environ.get("AUTH_HEADER") or "X-Forwarded-Preferred-Username"
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
def split_url(url: str):
|
||||
parsed = urlparse(url)
|
||||
|
||||
http_vhost = parsed.hostname
|
||||
http_uri = parsed.path or "/"
|
||||
http_ssl = parsed.scheme == "https"
|
||||
|
||||
return {
|
||||
"http_vhost": http_vhost,
|
||||
"http_uri": http_uri,
|
||||
"http_ssl": http_ssl
|
||||
}
|
||||
|
||||
class Service(db.Model):
|
||||
|
||||
__tablename__ = "services"
|
||||
@@ -44,6 +64,11 @@ class Service(db.Model):
|
||||
owner = Column(String)
|
||||
special_type = Column(String)
|
||||
|
||||
# web checks #
|
||||
url = Column(String)
|
||||
accepted_codes = Column(String)
|
||||
http_expect = Column(String)
|
||||
|
||||
staticly_configured = Column(Boolean)
|
||||
|
||||
class Status(db.Model):
|
||||
@@ -56,7 +81,7 @@ class Status(db.Model):
|
||||
info_text = Column(String)
|
||||
|
||||
def human_date(self):
|
||||
dt = datetime.datetime.fromtimestamp(self.timestamp)
|
||||
dt = datetime.datetime.fromtimestamp(self.timestamp, app.config["TIME_ZONE"])
|
||||
return dt.strftime("%d. %B %Y at %H:%M")
|
||||
|
||||
class SMARTStatus(db.Model):
|
||||
@@ -91,7 +116,7 @@ def buildReponseDict(status, service=None):
|
||||
@app.route('/overview')
|
||||
def overview():
|
||||
|
||||
user = str(flask.request.headers.get("X-Forwarded-Preferred-Username"))
|
||||
user = str(flask.request.headers.get(app.config['AUTH_HEADER']))
|
||||
|
||||
# query all services #
|
||||
services = db.session.query(Service).filter(Service.owner == user).all()
|
||||
@@ -161,7 +186,7 @@ def create_entry(form, user):
|
||||
@app.route("/service-details")
|
||||
def service_details():
|
||||
|
||||
user = str(flask.request.headers.get("X-Forwarded-Preferred-Username"))
|
||||
user = flask.request.headers.get(app.config['AUTH_HEADER'])
|
||||
service = flask.request.args.get("service")
|
||||
|
||||
# query service #
|
||||
@@ -174,8 +199,28 @@ def service_details():
|
||||
return ("Services is not owned by {}".format(user))
|
||||
|
||||
status_list_query = db.session.query(Status).filter(Status.service==service.service)
|
||||
status_list = status_list_query.order_by(sqlalchemy.desc(Status.timestamp)).limit(20).all()
|
||||
status_list = status_list_query.order_by(sqlalchemy.desc(Status.timestamp)).limit(200).all()
|
||||
|
||||
# build status tupel (repeats, status) #
|
||||
current_tupel = None
|
||||
prev_status = None
|
||||
tupel_list = []
|
||||
for s in status_list:
|
||||
|
||||
# set initial #
|
||||
if not current_tupel:
|
||||
current_tupel = [1, s]
|
||||
tupel_list.append(current_tupel)
|
||||
continue
|
||||
|
||||
if current_tupel[1].info_text == s.info_text:
|
||||
current_tupel[0] += 1
|
||||
else:
|
||||
current_tupel = [1, s]
|
||||
tupel_list.append(current_tupel)
|
||||
|
||||
|
||||
print(tupel_list)
|
||||
icinga_link = icingatools.build_icinga_link_for_service(user, service.service,
|
||||
service.staticly_configured, app)
|
||||
|
||||
@@ -183,13 +228,13 @@ def service_details():
|
||||
smart_entry = smart_entry_list.order_by(SMARTStatus.timestamp.desc()).first()
|
||||
|
||||
return flask.render_template("service_info.html", service=service, flask=flask,
|
||||
user=user, status_list=status_list, icinga_link=icinga_link, smart=smart_entry)
|
||||
user=user, status_list=tupel_list, icinga_link=icinga_link, smart=smart_entry)
|
||||
|
||||
|
||||
@app.route("/entry-form", methods=["GET", "POST", "DELETE"])
|
||||
def create_interface():
|
||||
|
||||
user = str(flask.request.headers.get("X-Forwarded-Preferred-Username"))
|
||||
user = flask.request.headers.get(app.config['AUTH_HEADER'])
|
||||
|
||||
# check if is delete #
|
||||
operation = flask.request.args.get("operation")
|
||||
@@ -224,9 +269,21 @@ def create_interface():
|
||||
return ("Not a valid service to modify", 404)
|
||||
|
||||
if flask.request.method == "POST":
|
||||
create_entry(form, user)
|
||||
|
||||
try:
|
||||
create_entry(form, user)
|
||||
except IntegrityError as e:
|
||||
db.session.rollback()
|
||||
# TODO: this only works for PG
|
||||
if isinstance(e.orig, UniqueViolation):
|
||||
return ("A service with this name already exists (possibly by another user)", 409)
|
||||
else:
|
||||
return (f"Error: {e}", 500)
|
||||
|
||||
# service created successfully #
|
||||
service_name = form.service.data or form.service_hidden.data
|
||||
return flask.redirect('/service-details?service={}'.format(service_name))
|
||||
|
||||
else:
|
||||
return flask.render_template('add_modify_service.html', form=form,
|
||||
is_modification=bool(modify_service_name))
|
||||
@@ -345,7 +402,17 @@ def default():
|
||||
status = Status(service=service, timestamp=timestamp, status=status,
|
||||
info_text=text)
|
||||
db.session.merge(status)
|
||||
db.session.commit()
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
except IntegrityError as e:
|
||||
db.session.rollback()
|
||||
# TODO: this only works for PG
|
||||
if isinstance(e.orig, UniqueViolation):
|
||||
return ("Status at this time already submitted", 409)
|
||||
else:
|
||||
return (f"Error: {e}", 500)
|
||||
|
||||
return ("", 204)
|
||||
else:
|
||||
return ("Method not implemented: {}".format(flask.request.method), 405)
|
||||
@@ -448,8 +515,7 @@ def create_app():
|
||||
config |= json.load(f)
|
||||
|
||||
if not config:
|
||||
print("No valid configuration found - need at least one service")
|
||||
return
|
||||
print("No static services configuration found - loading finished.")
|
||||
|
||||
for key in config:
|
||||
timeout = timeparse.timeparse(config[key]["timeout"])
|
||||
@@ -459,12 +525,34 @@ def create_app():
|
||||
owner=config[key]["owner"]))
|
||||
db.session.commit()
|
||||
|
||||
LOAD_FROM_ENV = [
|
||||
"ICINGA_API_USER",
|
||||
"ICINGA_API_PASS",
|
||||
"ICINGA_API_URL",
|
||||
"ICINGA_WEB_URL",
|
||||
"ASYNC_ICINGA_DUMMY_HOST"
|
||||
]
|
||||
|
||||
enforce_load_from_env = os.environ.get("ENFORCE_LOAD_FROM_ENV") or ""
|
||||
missing = [k for k in LOAD_FROM_ENV if k not in os.environ]
|
||||
if missing and enforce_load_from_env.lower() == "true":
|
||||
print(f"ENFORCE_LOAD_FROM_ENV is 'true' but we are missing: {missing} - Abort.")
|
||||
sys.exit(1)
|
||||
|
||||
for key in LOAD_FROM_ENV:
|
||||
if key in os.environ:
|
||||
print(f"Loading/Overwriting {key} from environment", file=sys.stderr)
|
||||
app.config[key] = os.environ[key]
|
||||
|
||||
|
||||
# create icinga host #
|
||||
if not app.config.get("ICINGA_API_URL"):
|
||||
print("ICINGA_API_URL not defined. Not connecting Icinga", file=sys.stderr)
|
||||
else:
|
||||
icingatools.create_master_host(app)
|
||||
|
||||
print(f"Expected AUTH_HEADER is: {app.config['AUTH_HEADER']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
<a href="/service-details?service={{ status.service}}"
|
||||
class="col-md-5 m-3 p-2 border rounded overview-tile"
|
||||
{% if status.status == "OK" %}
|
||||
style="background-color: lightgreen;"
|
||||
{% if status.info_text == "Submitted from Web-Interface" %}
|
||||
style="background-color: #5cffe0;"
|
||||
{% else %}
|
||||
style="background-color: lightgreen;"
|
||||
{% endif %}
|
||||
{% elif status.status == "WARNING" %}
|
||||
style="background-color: orange;"
|
||||
{% elif status.status == "CRITICAL" %}
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
|
||||
<div class="last-status">
|
||||
{% if status_list | length > 0 %}
|
||||
<p class="{{ status_list[0].status }}">
|
||||
{{ status_list[0].status }} submitted on {{ status_list[0].human_date() }}
|
||||
<p class="{{ status_list[0][1].status }}">
|
||||
{{ status_list[0][1].status }} submitted on {{ status_list[0][1].human_date() }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p style="color: darkred;">No status for this service submitted</p>
|
||||
@@ -169,12 +169,19 @@
|
||||
</thead>
|
||||
|
||||
<tbody class="mt-2">
|
||||
{% for status in status_list %}
|
||||
{% for status_tupel in status_list %}
|
||||
<tr>
|
||||
<td>{{ status.human_date() }}</td>
|
||||
<td class="{{ status.status }}">{{ status.status }}</td>
|
||||
<td>{{ status.info_text }}</td>
|
||||
<td>{{ status_tupel[1].human_date() }}</td>
|
||||
<td class="{{ status_tupel[1].status }}">{{ status_tupel[1].status }}</td>
|
||||
<td>{{ status_tupel[1].info_text }}</td>
|
||||
</tr>
|
||||
{% if status_tupel[0] > 1 %}
|
||||
<tr>
|
||||
<td>---</td>
|
||||
<td><i> + {{ status_tupel[0] }} identical reports</i></td>
|
||||
<td>|</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user