Compare commits

...

5 Commits

Author SHA1 Message Date
b0752c2984 fix: different color if submitted from web interface
All checks were successful
ci / docker (push) Successful in 3m25s
2026-04-20 23:17:44 +02:00
1af07b90cc feat: group identical reports 2026-04-20 23:12:49 +02:00
ed039833c5 wip: icingatools webcheck 2026-04-20 23:12:34 +02:00
bb5aaa47ad fix: correct rollback problem
All checks were successful
ci / docker (push) Successful in 52s
2026-03-29 13:06:22 +02:00
87b8de01d7 fix: better handling of duplicates 2026-03-29 12:54:36 +02:00
4 changed files with 132 additions and 24 deletions

View File

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

View File

@@ -23,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
@@ -37,6 +39,21 @@ 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"
@@ -47,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):
@@ -177,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)
@@ -186,7 +228,7 @@ 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"])
@@ -227,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))
@@ -348,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)

View File

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

View File

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