mirror of
https://github.com/FAUSheppy/icinga-webhook-gateway
synced 2026-04-26 22:22:30 +02:00
Compare commits
28 Commits
smart-dev
...
bb5aaa47ad
| Author | SHA1 | Date | |
|---|---|---|---|
| bb5aaa47ad | |||
| 87b8de01d7 | |||
| ef8e1e6a81 | |||
| 6680f4769c | |||
| 95c3551a5c | |||
| 31db0c22d2 | |||
| ce5328da53 | |||
| 1d36a9aaed | |||
| 7fea3bf315 | |||
| 2e37ddcb8e | |||
| 7d612c0ccd | |||
| 52569c7687 | |||
| d70a37f42c | |||
| 8d6590364f | |||
| dd7a81fd0f | |||
| 7b5f28651b | |||
| a7f4788291 | |||
| 74b48a2477 | |||
| 18f8436078 | |||
| 3df3ddb08e | |||
| 72e0210d26 | |||
| edc454f154 | |||
| 824c108678 | |||
| 08fc17efe0 | |||
| 683ebefbb0 | |||
| 0842818cbc | |||
| d6ea667733 | |||
| 935bfa3eef |
5
.github/workflows/main.yaml
vendored
5
.github/workflows/main.yaml
vendored
@@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "master"
|
- "master"
|
||||||
|
schedule:
|
||||||
|
- cron: "0 2 * * 0"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
@@ -14,9 +16,6 @@ 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
|
||||||
|
|||||||
2
req.txt
2
req.txt
@@ -5,3 +5,5 @@ flask-wtf
|
|||||||
waitress
|
waitress
|
||||||
requests
|
requests
|
||||||
icinga2api
|
icinga2api
|
||||||
|
psycopg2-binary
|
||||||
|
tzdata
|
||||||
|
|||||||
71
server.py
71
server.py
@@ -9,6 +9,7 @@ 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
|
||||||
@@ -22,6 +23,8 @@ 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
|
||||||
@@ -29,9 +32,11 @@ import smarttools
|
|||||||
app = flask.Flask("Icinga Report In Gateway")
|
app = flask.Flask("Icinga Report In Gateway")
|
||||||
|
|
||||||
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.sqlite'
|
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_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)
|
||||||
|
|
||||||
class Service(db.Model):
|
class Service(db.Model):
|
||||||
@@ -56,7 +61,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)
|
dt = datetime.datetime.fromtimestamp(self.timestamp, app.config["TIME_ZONE"])
|
||||||
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):
|
||||||
@@ -65,12 +70,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)
|
||||||
@@ -91,7 +96,7 @@ def buildReponseDict(status, service=None):
|
|||||||
@app.route('/overview')
|
@app.route('/overview')
|
||||||
def 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 #
|
# query all services #
|
||||||
services = db.session.query(Service).filter(Service.owner == user).all()
|
services = db.session.query(Service).filter(Service.owner == user).all()
|
||||||
@@ -161,7 +166,7 @@ def create_entry(form, user):
|
|||||||
@app.route("/service-details")
|
@app.route("/service-details")
|
||||||
def 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")
|
service = flask.request.args.get("service")
|
||||||
|
|
||||||
# query service #
|
# query service #
|
||||||
@@ -189,7 +194,7 @@ def service_details():
|
|||||||
@app.route("/entry-form", methods=["GET", "POST", "DELETE"])
|
@app.route("/entry-form", methods=["GET", "POST", "DELETE"])
|
||||||
def create_interface():
|
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 #
|
# check if is delete #
|
||||||
operation = flask.request.args.get("operation")
|
operation = flask.request.args.get("operation")
|
||||||
@@ -224,9 +229,21 @@ 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":
|
||||||
|
|
||||||
|
try:
|
||||||
create_entry(form, user)
|
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))
|
||||||
@@ -345,7 +362,17 @@ 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)
|
||||||
|
|
||||||
|
try:
|
||||||
db.session.commit()
|
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)
|
||||||
@@ -396,7 +423,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 > 50:
|
if smart_last.temperature > 60:
|
||||||
return ("Disk Temperatur {}".format(smart_last.temperature), "CRITICAL")
|
return ("Disk Temperatur {}".format(smart_last.temperature), "CRITICAL")
|
||||||
|
|
||||||
# available_SSD spare #
|
# available_SSD spare #
|
||||||
@@ -412,11 +439,12 @@ def record_and_check_smart(service, timestamp, smart):
|
|||||||
spare_change), "WARNING")
|
spare_change), "WARNING")
|
||||||
|
|
||||||
# unsafe_shutdowns +1 #
|
# unsafe_shutdowns +1 #
|
||||||
if smart_second_last.unsafe_shutdowns - smart_last.unsafe_shutdowns >= 1:
|
if(smart_second_last and
|
||||||
|
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 ("", "OK")
|
return ("{} - no problems detected".format(smart_last.model_number), "OK")
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
@@ -447,8 +475,7 @@ def create_app():
|
|||||||
config |= json.load(f)
|
config |= json.load(f)
|
||||||
|
|
||||||
if not config:
|
if not config:
|
||||||
print("No valid configuration found - need at least one service")
|
print("No static services configuration found - loading finished.")
|
||||||
return
|
|
||||||
|
|
||||||
for key in config:
|
for key in config:
|
||||||
timeout = timeparse.timeparse(config[key]["timeout"])
|
timeout = timeparse.timeparse(config[key]["timeout"])
|
||||||
@@ -458,12 +485,34 @@ 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__":
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ 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:
|
||||||
@@ -46,4 +48,7 @@ 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,5 +1,6 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,13 +83,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if smart %}
|
{% if service.special_type == "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 smart %}
|
{% if service.special_type == "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 smart %}
|
{% if service.special_type == "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,6 +128,22 @@
|
|||||||
} | 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' > $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">
|
||||||
|
|||||||
Reference in New Issue
Block a user