9 Commits

Author SHA1 Message Date
sheppy d5d964ea8e wip: 2024-01-03 13:40:42 +01:00
sheppy 8d4882d7c4 wip: 2024-01-03 12:39:16 +01:00
sheppy 6ca2278838 wip: 2024-01-03 12:25:01 +01:00
sheppy 8884e924e8 wip: 2024-01-03 12:24:54 +01:00
sheppy 9eef4b1353 wip: 2024-01-03 01:21:34 +01:00
sheppy ebb644fdf2 wip: 2024-01-03 00:48:44 +01:00
sheppy c487c769b0 wip: 2024-01-02 13:54:04 +01:00
sheppy ff2d8296f3 wip: 2024-01-02 12:31:06 +01:00
sheppy ad08f69df1 wip: 2024-01-01 20:41:29 +01:00
8 changed files with 43 additions and 208 deletions
+3 -2
View File
@@ -4,8 +4,6 @@ on:
push: push:
branches: branches:
- "master" - "master"
schedule:
- cron: "0 2 * * 0"
jobs: jobs:
docker: docker:
@@ -16,6 +14,9 @@ jobs:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
+13 -46
View File
@@ -1,15 +1,5 @@
import icinga2api import icinga2api
import icinga2api.client 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): def _create_client(app):
@@ -49,7 +39,7 @@ def _build_service_name(user, async_service_name):
return "{}_async_{}".format(user, async_service_name) return "{}_async_{}".format(user, async_service_name)
def create_service(user, async_service_name, app, webcheck=False): def create_service(user, async_service_name, app):
if not app.config.get("ICINGA_API_URL"): if not app.config.get("ICINGA_API_URL"):
return return
@@ -58,43 +48,20 @@ def create_service(user, async_service_name, app, webcheck=False):
name = _build_service_name(user, async_service_name) name = _build_service_name(user, async_service_name)
host_name = app.config["ASYNC_ICINGA_DUMMY_HOST"] host_name = app.config["ASYNC_ICINGA_DUMMY_HOST"]
# TODO: query service from DB service_config = {
accepted_return_codes = [200, 204] "templates": ["generic-service"],
"attrs": {
"display_name": name,
if webcheck: "check_command": "gateway",
http_vhost, http_uri, http_ssl = split_url(url) "host_name" : host_name,
service_config = { "vars" : {
"templates": ["generic-service"], "host" : "async-icinga.atlantishq.de",
"attrs": { "service_name" : async_service_name,
"display_name": name, "protocol" : "https",
"check_command": "http", "owner" : user
"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) # Create the service (name is required in this format)
service_api_helper_name = "{}!{}".format(host_name, name) service_api_helper_name = "{}!{}".format(host_name, name)
-2
View File
@@ -5,5 +5,3 @@ flask-wtf
waitress waitress
requests requests
icinga2api icinga2api
psycopg2-binary
tzdata
+16 -114
View File
@@ -9,22 +9,19 @@ import datetime
import pytimeparse.timeparse as timeparse import pytimeparse.timeparse as timeparse
import sys import sys
import secrets import secrets
import zoneinfo
import flask_wtf import flask_wtf
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, BooleanField, DecimalField, HiddenField, SelectField from wtforms import StringField, SubmitField, BooleanField, DecimalField, HiddenField, SelectField
from wtforms.validators import DataRequired, Length from wtforms.validators import DataRequired, Length
from sqlalchemy import Column, Integer, String, Boolean, or_, and_, desc from sqlalchemy import Column, Integer, String, Boolean, or_, and_
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func from sqlalchemy.sql import func
import sqlalchemy import sqlalchemy
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from sqlalchemy.exc import IntegrityError
from psycopg2.errors import UniqueViolation
import icingatools import icingatools
import smarttools import smarttools
@@ -32,28 +29,11 @@ import smarttools
app = flask.Flask("Icinga Report In Gateway") app = flask.Flask("Icinga Report In Gateway")
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI') or 'sqlite:///database.sqlite' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.sqlite'
app.config['JSON_CONFIG_FILE'] = 'services.json' app.config['JSON_CONFIG_FILE'] = 'services.json'
app.config['JSON_CONFIG_DIR'] = 'config' 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) 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): class Service(db.Model):
__tablename__ = "services" __tablename__ = "services"
@@ -64,11 +44,6 @@ class Service(db.Model):
owner = Column(String) owner = Column(String)
special_type = Column(String) special_type = Column(String)
# web checks #
url = Column(String)
accepted_codes = Column(String)
http_expect = Column(String)
staticly_configured = Column(Boolean) staticly_configured = Column(Boolean)
class Status(db.Model): class Status(db.Model):
@@ -81,7 +56,7 @@ class Status(db.Model):
info_text = Column(String) info_text = Column(String)
def human_date(self): def human_date(self):
dt = datetime.datetime.fromtimestamp(self.timestamp, app.config["TIME_ZONE"]) dt = datetime.datetime.fromtimestamp(self.timestamp)
return dt.strftime("%d. %B %Y at %H:%M") return dt.strftime("%d. %B %Y at %H:%M")
class SMARTStatus(db.Model): class SMARTStatus(db.Model):
@@ -90,12 +65,12 @@ class SMARTStatus(db.Model):
service = Column(String, primary_key=True) service = Column(String, primary_key=True)
timestamp = Column(Integer, primary_key=True) timestamp = Column(Integer, primary_key=True)
model_number = Column(String, primary_key=True)
power_cycles = Column(Integer) power_cycles = Column(Integer)
temperature = Column(Integer) temperature = Column(Integer)
available_spare = Column(Integer) available_spare = Column(Integer)
unsafe_shutdowns = Column(Integer) unsafe_shutdowns = Column(Integer)
critical_warning = Column(Integer) critical_warning = Column(Integer)
model_number = Column(String)
power_cycles = Column(Integer) power_cycles = Column(Integer)
power_on_hours = Column(Integer) power_on_hours = Column(Integer)
wearleveling_count = Column(Integer) wearleveling_count = Column(Integer)
@@ -116,7 +91,7 @@ def buildReponseDict(status, service=None):
@app.route('/overview') @app.route('/overview')
def overview(): def overview():
user = str(flask.request.headers.get(app.config['AUTH_HEADER'])) user = str(flask.request.headers.get("X-Forwarded-Preferred-Username"))
# query all services # # query all services #
services = db.session.query(Service).filter(Service.owner == user).all() services = db.session.query(Service).filter(Service.owner == user).all()
@@ -186,7 +161,7 @@ def create_entry(form, user):
@app.route("/service-details") @app.route("/service-details")
def service_details(): def service_details():
user = flask.request.headers.get(app.config['AUTH_HEADER']) user = str(flask.request.headers.get("X-Forwarded-Preferred-Username"))
service = flask.request.args.get("service") service = flask.request.args.get("service")
# query service # # query service #
@@ -198,38 +173,9 @@ def service_details():
if service.owner and str(service.owner) != user: if service.owner and str(service.owner) != user:
return ("Services is not owned by {}".format(user)) return ("Services is not owned by {}".format(user))
two_weeks_ago_ts = int((datetime.datetime.now() - datetime.timedelta(days=14)).timestamp())
status_list_query = db.session.query(Status).filter(Status.service==service.service) status_list_query = db.session.query(Status).filter(Status.service==service.service)
recent_query = ( status_list = status_list_query.order_by(sqlalchemy.desc(Status.timestamp)).limit(20).all()
status_list_query
.filter(Status.timestamp >= two_weeks_ago_ts, Status.service==service.service)
.order_by(desc(Status.timestamp))
).limit(1000)
status_list = recent_query.all()
if not status_list:
status_list = status_list_query.order_by(sqlalchemy.desc(Status.timestamp)).limit(1000).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, icinga_link = icingatools.build_icinga_link_for_service(user, service.service,
service.staticly_configured, app) service.staticly_configured, app)
@@ -237,13 +183,13 @@ def service_details():
smart_entry = smart_entry_list.order_by(SMARTStatus.timestamp.desc()).first() smart_entry = smart_entry_list.order_by(SMARTStatus.timestamp.desc()).first()
return flask.render_template("service_info.html", service=service, flask=flask, return flask.render_template("service_info.html", service=service, flask=flask,
user=user, status_list=tupel_list, icinga_link=icinga_link, smart=smart_entry) user=user, status_list=status_list, icinga_link=icinga_link, smart=smart_entry)
@app.route("/entry-form", methods=["GET", "POST", "DELETE"]) @app.route("/entry-form", methods=["GET", "POST", "DELETE"])
def create_interface(): def create_interface():
user = flask.request.headers.get(app.config['AUTH_HEADER']) user = str(flask.request.headers.get("X-Forwarded-Preferred-Username"))
# check if is delete # # check if is delete #
operation = flask.request.args.get("operation") operation = flask.request.args.get("operation")
@@ -278,21 +224,9 @@ def create_interface():
return ("Not a valid service to modify", 404) return ("Not a valid service to modify", 404)
if flask.request.method == "POST": 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 service_name = form.service.data or form.service_hidden.data
return flask.redirect('/service-details?service={}'.format(service_name)) return flask.redirect('/service-details?service={}'.format(service_name))
else: else:
return flask.render_template('add_modify_service.html', form=form, return flask.render_template('add_modify_service.html', form=form,
is_modification=bool(modify_service_name)) is_modification=bool(modify_service_name))
@@ -411,17 +345,7 @@ def default():
status = Status(service=service, timestamp=timestamp, status=status, status = Status(service=service, timestamp=timestamp, status=status,
info_text=text) info_text=text)
db.session.merge(status) 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) return ("", 204)
else: else:
return ("Method not implemented: {}".format(flask.request.method), 405) return ("Method not implemented: {}".format(flask.request.method), 405)
@@ -472,7 +396,7 @@ def record_and_check_smart(service, timestamp, smart):
return ("SMART report prefail disk (wear_level < 20%)", "CRITICAL") return ("SMART report prefail disk (wear_level < 20%)", "CRITICAL")
# temp max > X # # temp max > X #
if smart_last.temperature > 60: if smart_last.temperature > 50:
return ("Disk Temperatur {}".format(smart_last.temperature), "CRITICAL") return ("Disk Temperatur {}".format(smart_last.temperature), "CRITICAL")
# available_SSD spare # # available_SSD spare #
@@ -488,12 +412,11 @@ def record_and_check_smart(service, timestamp, smart):
spare_change), "WARNING") spare_change), "WARNING")
# unsafe_shutdowns +1 # # unsafe_shutdowns +1 #
if(smart_second_last and if smart_second_last.unsafe_shutdowns - smart_last.unsafe_shutdowns >= 1:
smart_second_last.unsafe_shutdowns - smart_last.unsafe_shutdowns >= 1):
return ("Disk had {} unsafe shutdowns".format(smart_last.unsafe_shutdowns), return ("Disk had {} unsafe shutdowns".format(smart_last.unsafe_shutdowns),
"WARNING") "WARNING")
return ("{} - no problems detected".format(smart_last.model_number), "OK") return ("", "OK")
def create_app(): def create_app():
@@ -524,7 +447,8 @@ def create_app():
config |= json.load(f) config |= json.load(f)
if not config: if not config:
print("No static services configuration found - loading finished.") print("No valid configuration found - need at least one service")
return
for key in config: for key in config:
timeout = timeparse.timeparse(config[key]["timeout"]) timeout = timeparse.timeparse(config[key]["timeout"])
@@ -534,34 +458,12 @@ def create_app():
owner=config[key]["owner"])) owner=config[key]["owner"]))
db.session.commit() 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 # # create icinga host #
if not app.config.get("ICINGA_API_URL"): if not app.config.get("ICINGA_API_URL"):
print("ICINGA_API_URL not defined. Not connecting Icinga", file=sys.stderr) print("ICINGA_API_URL not defined. Not connecting Icinga", file=sys.stderr)
else: else:
icingatools.create_master_host(app) icingatools.create_master_host(app)
print(f"Expected AUTH_HEADER is: {app.config['AUTH_HEADER']}")
if __name__ == "__main__": if __name__ == "__main__":
-5
View File
@@ -34,8 +34,6 @@ def normalize(smart):
elif name == "power_on_hours": elif name == "power_on_hours":
target_name = "power_on_hours" target_name = "power_on_hours"
use_raw = True use_raw = True
elif name == "perc_avail_resrvd_space":
target_name = "available_spare"
# check if metric should be recorded # # check if metric should be recorded #
if target_name in ret: if target_name in ret:
@@ -48,7 +46,4 @@ def normalize(smart):
ret[target_name] = value ret[target_name] = value
if ret["critical_warning"] == 0 and "smart_status" in smart:
ret["critical_warning"] = int(not smart["smart_status"]["passed"])
return ret return ret
-1
View File
@@ -1,6 +1,5 @@
body{ body{
background: radial-gradient(ellipse at center, #47918a 0%, #0b3161 100%); background: radial-gradient(ellipse at center, #47918a 0%, #0b3161 100%);
background-attachment: fixed;
color: aliceblue !important; color: aliceblue !important;
} }
+1 -5
View File
@@ -8,11 +8,7 @@
<a href="/service-details?service={{ status.service}}" <a href="/service-details?service={{ status.service}}"
class="col-md-5 m-3 p-2 border rounded overview-tile" class="col-md-5 m-3 p-2 border rounded overview-tile"
{% if status.status == "OK" %} {% if status.status == "OK" %}
{% if status.info_text == "Submitted from Web-Interface" %} style="background-color: lightgreen;"
style="background-color: #5cffe0;"
{% else %}
style="background-color: lightgreen;"
{% endif %}
{% elif status.status == "WARNING" %} {% elif status.status == "WARNING" %}
style="background-color: orange;" style="background-color: orange;"
{% elif status.status == "CRITICAL" %} {% elif status.status == "CRITICAL" %}
+9 -32
View File
@@ -56,8 +56,8 @@
<div class="last-status"> <div class="last-status">
{% if status_list | length > 0 %} {% if status_list | length > 0 %}
<p class="{{ status_list[0][1].status }}"> <p class="{{ status_list[0].status }}">
{{ status_list[0][1].status }} submitted on {{ status_list[0][1].human_date() }} {{ status_list[0].status }} submitted on {{ status_list[0].human_date() }}
</p> </p>
{% else %} {% else %}
<p style="color: darkred;">No status for this service submitted</p> <p style="color: darkred;">No status for this service submitted</p>
@@ -83,13 +83,13 @@
</div> </div>
{% endif %} {% endif %}
{% if service.special_type == "SMART" %} {% if smart %}
<h5 class="clear my-4">Linux</h5> <h5 class="clear my-4">Linux</h5>
{% else %} {% else %}
<h5 class="clear my-4">Curl</h5> <h5 class="clear my-4">Curl</h5>
{% endif %} {% endif %}
<div class="ml-3 example"> <div class="ml-3 example">
{% if service.special_type == "SMART" %} {% if smart %}
SMART='{ <br> SMART='{ <br>
<div class="example-indent"> <div class="example-indent">
"service" : "{{ service.service }}", <br> "service" : "{{ service.service }}", <br>
@@ -115,7 +115,7 @@
{% endif %} {% endif %}
</div> </div>
{% if service.special_type == "SMART" %} {% if smart %}
<h5 class="my-4">Windows</h5> <h5 class="my-4">Windows</h5>
<div class="ml-3 example"> <div class="ml-3 example">
$SMART = @{ <br> $SMART = @{ <br>
@@ -128,22 +128,6 @@
} | ConvertTo-Json<br><br> } | ConvertTo-Json<br><br>
Invoke-RestMethod -TimeoutSec 2 -Uri "{{ flask.request.url_root.replace("http://", "https://" )}}report" -Method Post -Headers @{"Content-Type"="application/json"} -Body $SMART Invoke-RestMethod -TimeoutSec 2 -Uri "{{ flask.request.url_root.replace("http://", "https://" )}}report" -Method Post -Headers @{"Content-Type"="application/json"} -Body $SMART
</div> </div>
<!-- register task example -->
<h5 class="my-4">Windows Task (requires Admin-Powershell)</h5>
<div class="ml-3 example">
$ScriptPath = Join-Path $HOME -ChildPath "smart_monitor.ps1" <br>
echo '$SMART = @{ <br>
<div class="example-indent">
service = "{{ service.service }}"<br>
token = "{{ service.token }}"<br>
status = "N/A"<br>
smart = "$(smartctl -a C: --json | Out-String)"<br>
</div>
} | ConvertTo-Json<br><br>
Invoke-RestMethod -TimeoutSec 2 -Uri "{{ flask.request.url_root.replace("http://", "https://" )}}report" -Method Post -Headers @{"Content-Type"="application/json"} -Body $SMART' &gt; $ScriptPath <br>
schtasks /create /tn SMART_Monitor /tr "powershell.exe -executionpolicy bypass -File '$ScriptPath'" /sc hourly /mo 1 /ru "Administratoren"<br>
echo "Done" <br>
</div>
{% else %} {% else %}
<h5 class="my-4">Python</h5> <h5 class="my-4">Python</h5>
<div class="ml-3 example"> <div class="ml-3 example">
@@ -169,19 +153,12 @@
</thead> </thead>
<tbody class="mt-2"> <tbody class="mt-2">
{% for status_tupel in status_list %} {% for status in status_list %}
<tr> <tr>
<td>{{ status_tupel[1].human_date() }}</td> <td>{{ status.human_date() }}</td>
<td class="{{ status_tupel[1].status }}">{{ status_tupel[1].status }}</td> <td class="{{ status.status }}">{{ status.status }}</td>
<td>{{ status_tupel[1].info_text }}</td> <td>{{ status.info_text }}</td>
</tr> </tr>
{% if status_tupel[0] > 1 %}
<tr>
<td>---</td>
<td><i> + {{ status_tupel[0] }} identical reports</i></td>
<td>|</td>
</tr>
{% endif %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>