mirror of
https://github.com/FAUSheppy/icinga-webhook-gateway
synced 2025-12-07 07:51:40 +01:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5d964ea8e | |||
| 8d4882d7c4 | |||
| 6ca2278838 | |||
| 8884e924e8 | |||
| 9eef4b1353 | |||
| ebb644fdf2 | |||
| c487c769b0 | |||
| ff2d8296f3 | |||
| ad08f69df1 | |||
| 1860cba273 | |||
| 95db2f4b9f | |||
| 5439f46bd3 | |||
| 163854d7bd | |||
| c1addbf9fb | |||
| 3b89f534dd | |||
| 6c1afa82ff | |||
| cfab1e499c | |||
| fed1eab6d0 | |||
| 8be2e3d054 | |||
| c93ab82119 | |||
| 5b49bb2b37 | |||
| c4ed500961 | |||
| 9ac05997d3 | |||
| fb1356bbb4 | |||
| ef6b3fa5fb | |||
| 0fb170c97e | |||
| 3ba958f6e5 | |||
| 2d221a38c6 | |||
| 696f190667 | |||
| 5449fd1437 | |||
| e086a221b7 | |||
| 7ea5578132 | |||
| bb1c2a25e3 | |||
| a726032cf6 | |||
| 8c39628a51 | |||
| 876e3101c0 | |||
| 0d6e2aef7b | |||
| b879bd0a84 | |||
| 4653b5b425 | |||
| fcb410e947 |
2
.github/workflows/main.yaml
vendored
2
.github/workflows/main.yaml
vendored
@@ -34,4 +34,4 @@ jobs:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: "${{ secrets.REGISTRY }}/athq/async-icinga:latest"
|
||||
tags: "${{ secrets.REGISTRY }}/atlantishq/async-icinga:latest"
|
||||
|
||||
17
Dockerfile
17
Dockerfile
@@ -1,25 +1,20 @@
|
||||
FROM python:3.9-slim-buster
|
||||
|
||||
RUN apt update
|
||||
RUN apt install python3-pip -y
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN apt install curl -y
|
||||
RUN apt autoremove -y
|
||||
RUN apt clean
|
||||
FROM alpine
|
||||
RUN apk add --no-cache py3-pip
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN python3 -m pip install waitress
|
||||
RUN python3 -m pip install --no-cache-dir --break-system-packages waitress
|
||||
|
||||
COPY req.txt .
|
||||
RUN python3 -m pip install --no-cache-dir -r req.txt
|
||||
RUN python3 -m pip install --no-cache-dir --break-system-packages -r req.txt
|
||||
|
||||
# precreate database directory for mount (will otherwise be created at before_first_request)
|
||||
COPY ./ .
|
||||
RUN mkdir /app/instance/
|
||||
|
||||
HEALTHCHECK --interval=1m --timeout=5s --start-period=10s \
|
||||
CMD /usr/bin/curl --fail http://localhost:5000/ || exit 1
|
||||
CMD /usr/bin/curl --fail http://localhost:5000/alive || exit 1
|
||||
EXPOSE 5000/tcp
|
||||
|
||||
ENTRYPOINT ["waitress-serve"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2021 Yannik Schmidt
|
||||
Copyright 2023 Yannik Schmidt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ if __name__ == "__main__":
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
urlBase = "{proto}://{host}:{port}/?service={service}"
|
||||
urlBase = "{proto}://{host}:{port}/report?service={service}"
|
||||
if not args.port:
|
||||
urlBase = "{proto}://{host}/?service={service}"
|
||||
urlBase = "{proto}://{host}/report?service={service}"
|
||||
|
||||
url = urlBase.format(proto=args.protocol, host=args.host,
|
||||
port=args.port, service=args.service)
|
||||
|
||||
@@ -41,6 +41,9 @@ def _build_service_name(user, async_service_name):
|
||||
|
||||
def create_service(user, async_service_name, app):
|
||||
|
||||
if not app.config.get("ICINGA_API_URL"):
|
||||
return
|
||||
|
||||
client = _create_client(app)
|
||||
name = _build_service_name(user, async_service_name)
|
||||
host_name = app.config["ASYNC_ICINGA_DUMMY_HOST"]
|
||||
@@ -54,7 +57,8 @@ def create_service(user, async_service_name, app):
|
||||
"vars" : {
|
||||
"host" : "async-icinga.atlantishq.de",
|
||||
"service_name" : async_service_name,
|
||||
"protocol" : "https"
|
||||
"protocol" : "https",
|
||||
"owner" : user
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +70,9 @@ def create_service(user, async_service_name, app):
|
||||
|
||||
def delete_service(user, async_service_name, app):
|
||||
|
||||
if not app.config.get("ICINGA_API_URL"):
|
||||
return
|
||||
|
||||
client = _create_client(app)
|
||||
name = _build_service_name(user, async_service_name)
|
||||
host_name = app.config["ASYNC_ICINGA_DUMMY_HOST"]
|
||||
@@ -73,11 +80,24 @@ def delete_service(user, async_service_name, app):
|
||||
|
||||
client.objects.delete("Service", service_api_helper_name)
|
||||
|
||||
def build_icinga_link_for_service(user, service_name, app):
|
||||
def build_icinga_link_for_service(user, service_name, static_configured, app):
|
||||
|
||||
name = _build_service_name(user, service_name)
|
||||
url_fmt = "{base}/icingaweb2/dashboard/#!/icingaweb2/monitoring/service/show?host={host}&service={service}"
|
||||
return url_fmt.format(base=app.config["ICINGA_WEB_URL"],
|
||||
host=app.config["ASYNC_ICINGA_DUMMY_HOST"],
|
||||
|
||||
if static_configured:
|
||||
url_fmt = "{base}/icingaweb2/monitoring/list/services?service={service}&modifyFilter=1"
|
||||
name = service_name
|
||||
|
||||
icinga_web_url = app.config.get("ICINGA_WEB_URL")
|
||||
if not icinga_web_url:
|
||||
icinga_web_url = "ICINGA_WEB_URL_NOT_SET:"
|
||||
|
||||
dummy_host=app.config.get("ASYNC_ICINGA_DUMMY_HOST")
|
||||
if not dummy_host:
|
||||
dummy_host = "ASYNC_ICINGA_DUMMY_HOST_NOT_SET:"
|
||||
|
||||
return url_fmt.format(base=icinga_web_url,
|
||||
host=dummy_host,
|
||||
service=name)
|
||||
|
||||
|
||||
2
req.txt
2
req.txt
@@ -1,7 +1,7 @@
|
||||
pytimeparse
|
||||
flask
|
||||
flask-sqlalchemy
|
||||
flasl-wtf
|
||||
flask-wtf
|
||||
waitress
|
||||
requests
|
||||
icinga2api
|
||||
|
||||
196
server.py
196
server.py
@@ -12,7 +12,7 @@ import secrets
|
||||
|
||||
import flask_wtf
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, BooleanField, DecimalField, HiddenField
|
||||
from wtforms import StringField, SubmitField, BooleanField, DecimalField, HiddenField, SelectField
|
||||
from wtforms.validators import DataRequired, Length
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Boolean, or_, and_
|
||||
@@ -24,6 +24,7 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy.sql.expression import func
|
||||
|
||||
import icingatools
|
||||
import smarttools
|
||||
|
||||
app = flask.Flask("Icinga Report In Gateway")
|
||||
|
||||
@@ -41,6 +42,7 @@ class Service(db.Model):
|
||||
token = Column(String)
|
||||
timeout = Column(Integer)
|
||||
owner = Column(String)
|
||||
special_type = Column(String)
|
||||
|
||||
staticly_configured = Column(Boolean)
|
||||
|
||||
@@ -57,6 +59,22 @@ class Status(db.Model):
|
||||
dt = datetime.datetime.fromtimestamp(self.timestamp)
|
||||
return dt.strftime("%d. %B %Y at %H:%M")
|
||||
|
||||
class SMARTStatus(db.Model):
|
||||
|
||||
__tablename__ = "smart"
|
||||
|
||||
service = Column(String, primary_key=True)
|
||||
timestamp = Column(Integer, primary_key=True)
|
||||
power_cycles = Column(Integer)
|
||||
temperature = Column(Integer)
|
||||
available_spare = Column(Integer)
|
||||
unsafe_shutdowns = Column(Integer)
|
||||
critical_warning = Column(Integer)
|
||||
model_number = Column(String)
|
||||
power_cycles = Column(Integer)
|
||||
power_on_hours = Column(Integer)
|
||||
wearleveling_count = Column(Integer)
|
||||
|
||||
def buildReponseDict(status, service=None):
|
||||
|
||||
if not status:
|
||||
@@ -108,19 +126,34 @@ class EntryForm(FlaskForm):
|
||||
|
||||
service = StringField("Service Name")
|
||||
service_hidden = HiddenField("service_hidden")
|
||||
special_type = SelectField("Type", choices=["Default", "SMART"])
|
||||
timeout = DecimalField("Timeout in days", default=30)
|
||||
|
||||
def create_entry(form, user):
|
||||
|
||||
# TODO add entry to icinga
|
||||
token = secrets.token_urlsafe(16)
|
||||
|
||||
service_name = form.service.data or form.service_hidden.data
|
||||
|
||||
service = Service(service=service_name, timeout=int(form.timeout.data),
|
||||
owner=user, token=token)
|
||||
# keep token if modification #
|
||||
s_tmp = db.session.query(Service).filter(Service.service == service_name).first()
|
||||
if s_tmp:
|
||||
token = s_tmp.token
|
||||
if not token:
|
||||
raise AssertionError("WTF Service without Token {}".format(service_name))
|
||||
|
||||
icingatools.create_service(user, service_name, app)
|
||||
day_delta = datetime.timedelta(days=int(form.timeout.data))
|
||||
|
||||
special_type = form.special_type.data
|
||||
if form.special_type == "Default":
|
||||
special_type = None
|
||||
|
||||
service = Service(service=service_name, timeout=day_delta.total_seconds(),
|
||||
owner=user, token=token, special_type=special_type)
|
||||
|
||||
# service.data set = create, service_hidden.data = modify #
|
||||
if form.service.data:
|
||||
icingatools.create_service(user, service_name, app)
|
||||
|
||||
db.session.merge(service)
|
||||
db.session.commit()
|
||||
@@ -128,7 +161,7 @@ def create_entry(form, user):
|
||||
@app.route("/service-details")
|
||||
def service_details():
|
||||
|
||||
user = flask.request.headers.get("X-Forwarded-Preferred-Username")
|
||||
user = str(flask.request.headers.get("X-Forwarded-Preferred-Username"))
|
||||
service = flask.request.args.get("service")
|
||||
|
||||
# query service #
|
||||
@@ -137,15 +170,20 @@ def service_details():
|
||||
# validate #
|
||||
if not service:
|
||||
return ("{} not found".format("service"), 404)
|
||||
if service.owner and service.owner != user:
|
||||
if service.owner and str(service.owner) != user:
|
||||
return ("Services is not owned by {}".format(user))
|
||||
|
||||
status_list = db.session.query(Status).filter(Status.service==service.service).all()
|
||||
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()
|
||||
|
||||
icinga_link = icingatools.build_icinga_link_for_service(user, service.service, app)
|
||||
icinga_link = icingatools.build_icinga_link_for_service(user, service.service,
|
||||
service.staticly_configured, app)
|
||||
|
||||
smart_entry_list = db.session.query(SMARTStatus).filter(SMARTStatus.service==service.service)
|
||||
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)
|
||||
user=user, status_list=status_list, icinga_link=icinga_link, smart=smart_entry)
|
||||
|
||||
|
||||
@app.route("/entry-form", methods=["GET", "POST", "DELETE"])
|
||||
@@ -154,7 +192,7 @@ def create_interface():
|
||||
user = str(flask.request.headers.get("X-Forwarded-Preferred-Username"))
|
||||
|
||||
# check if is delete #
|
||||
operation = flask.request.args.get("operation")
|
||||
operation = flask.request.args.get("operation")
|
||||
if operation and operation == "delete" :
|
||||
|
||||
service_delete_name = flask.request.args.get("service")
|
||||
@@ -171,19 +209,20 @@ def create_interface():
|
||||
return flask.redirect("/overview")
|
||||
|
||||
form = EntryForm()
|
||||
|
||||
|
||||
# handle modification #
|
||||
modify_service_name = flask.request.args.get("service")
|
||||
if modify_service_name:
|
||||
service = db.session.query(Service).filter(Service.service == modify_service_name).first()
|
||||
if service and service.owner == user:
|
||||
form.service.default = service.service
|
||||
form.timeout.default = service.timeout
|
||||
form.special_type.default = service.special_type
|
||||
form.timeout.default = datetime.timedelta(seconds=service.timeout).days
|
||||
form.service_hidden.default = service.service
|
||||
form.process()
|
||||
else:
|
||||
return ("Not a valid service to modify", 404)
|
||||
|
||||
|
||||
if flask.request.method == "POST":
|
||||
create_entry(form, user)
|
||||
service_name = form.service.data or form.service_hidden.data
|
||||
@@ -203,6 +242,7 @@ def reload():
|
||||
return ("", 204)
|
||||
|
||||
@app.route('/', methods=["GET", "POST"])
|
||||
@app.route('/report', methods=["GET", "POST"])
|
||||
def default():
|
||||
if flask.request.method == "GET":
|
||||
|
||||
@@ -247,7 +287,7 @@ def default():
|
||||
if not lastSuccess.timestamp == 0 and delta > timeout and latestInfoIsSuccess:
|
||||
|
||||
# lastes info is success but timed out #
|
||||
lastSuccess.info_text = "Service {} overdue since {}".format(service, str(delta))
|
||||
lastSuccess.info_text = "Service {} overdue since {}".format(service, str(delta))
|
||||
if timeout/delta > 0.9 or (delta - timeout) < datetime.timedelta(hours=12):
|
||||
lastSuccess.status = "WARNING"
|
||||
else:
|
||||
@@ -265,16 +305,27 @@ def default():
|
||||
elif flask.request.method == "POST":
|
||||
|
||||
# get variables #
|
||||
service = flask.request.json["service"]
|
||||
token = flask.request.json["token"]
|
||||
status = flask.request.json["status"]
|
||||
text = flask.request.json["info"]
|
||||
service = flask.request.json.get("service")
|
||||
token = flask.request.json.get("token")
|
||||
status = flask.request.json.get("status")
|
||||
text = flask.request.json.get("info") or "no_info"
|
||||
timestamp = datetime.datetime.now().timestamp()
|
||||
|
||||
smart = flask.request.json.get("smart")
|
||||
|
||||
# check smart json quoting problems #
|
||||
if smart and type(smart) == str:
|
||||
try:
|
||||
smart = json.loads(smart)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
return ("Error in SMART-json {}".format(e), 415)
|
||||
|
||||
if not service:
|
||||
return ("'service' ist empty field in json", 400)
|
||||
elif not token:
|
||||
return ("'token' ist empty field in json", 400)
|
||||
elif not status and not smart:
|
||||
return ("'status' is empty field in json", 400)
|
||||
|
||||
# verify token & service in config #
|
||||
verifiedServiceObj = db.session.query(Service).filter(
|
||||
@@ -283,33 +334,118 @@ def default():
|
||||
if not verifiedServiceObj:
|
||||
return ("Service ({}) with this token ({}) not found in DB".format(service, token), 401)
|
||||
else:
|
||||
status = Status(service=service, timestamp=timestamp, status=status, info_text=text)
|
||||
|
||||
# handle a SMART-record submission (with errorhandling) #
|
||||
if smart and not verifiedServiceObj.special_type == "SMART":
|
||||
return ("SMART Field for non-SMART type service", 415)
|
||||
elif smart:
|
||||
text, status = record_and_check_smart(verifiedServiceObj,
|
||||
timestamp, smart)
|
||||
|
||||
status = Status(service=service, timestamp=timestamp, status=status,
|
||||
info_text=text)
|
||||
db.session.merge(status)
|
||||
db.session.commit()
|
||||
return ("", 204)
|
||||
else:
|
||||
return ("Method not implemented: {}".format(flask.request.method), 405)
|
||||
|
||||
def record_and_check_smart(service, timestamp, smart):
|
||||
|
||||
if "nvme_smart_health_information_log" in smart:
|
||||
health_info = smart["nvme_smart_health_information_log"]
|
||||
else:
|
||||
health_info = smarttools.normalize(smart)
|
||||
|
||||
if not service.special_type == "SMART":
|
||||
raise AssertionError("Trying to record SMART-record for non-SMART service")
|
||||
|
||||
# record the status #
|
||||
smart_status = SMARTStatus(service=service.service, timestamp=timestamp,
|
||||
temperature=health_info["temperature"],
|
||||
critical_warning=health_info["critical_warning"],
|
||||
unsafe_shutdowns=health_info["unsafe_shutdowns"],
|
||||
power_cycles=health_info["power_cycles"],
|
||||
power_on_hours=health_info["power_on_hours"],
|
||||
available_spare=health_info.get("available_spare"),
|
||||
model_number=smart.get("model_name"),
|
||||
wearleveling_count=health_info.get("wearleveling_count"))
|
||||
|
||||
db.session.add(smart_status)
|
||||
db.session.commit()
|
||||
|
||||
# check the status #
|
||||
smart_last_query = db.session.query(SMARTStatus)
|
||||
smart_last_query = smart_last_query.filter(SMARTStatus.service==service.service)
|
||||
smart_last = smart_last_query.order_by(sqlalchemy.desc(SMARTStatus.timestamp)).first()
|
||||
smart_second_last = smart_last_query.order_by(sqlalchemy.desc(
|
||||
SMARTStatus.timestamp)).offset(1).first()
|
||||
|
||||
# last record (max 6 months ago) #
|
||||
timestampt_minus_6m = datetime.datetime.now() - datetime.timedelta(days=180)
|
||||
smart_old_query = smart_last_query.filter(
|
||||
SMARTStatus.timestamp > timestampt_minus_6m.timestamp())
|
||||
smart_old = smart_old_query.order_by(sqlalchemy.asc(SMARTStatus.timestamp)).first()
|
||||
|
||||
# critial != 0 #
|
||||
if smart_last.critical_warning != 0:
|
||||
return ("SMART reports disk critical => oO better do something about this", "CRITICAL")
|
||||
|
||||
# wearleveling < 20% (SAMSUNG only) #
|
||||
if smart_last.wearleveling_count and smart_last.wearleveling_count <= 20:
|
||||
return ("SMART report prefail disk (wear_level < 20%)", "CRITICAL")
|
||||
|
||||
# temp max > X #
|
||||
if smart_last.temperature > 50:
|
||||
return ("Disk Temperatur {}".format(smart_last.temperature), "CRITICAL")
|
||||
|
||||
# available_SSD spare #
|
||||
spare_change = smart_old.available_spare - smart_last.available_spare
|
||||
|
||||
if smart_last.available_spare <= 25:
|
||||
return ("SSD spare <25 ({}) YOUR DISK WILL DIE SOON".format(spare_change),
|
||||
"CRITICAL")
|
||||
elif smart_last.available_spare <= 50:
|
||||
return ("SSD spare <50 ({})".format(spare_change), "WARNING")
|
||||
elif spare_change >= 10:
|
||||
return ("Strong degration in SSD spare space ({} in under 6 months)".format(
|
||||
spare_change), "WARNING")
|
||||
|
||||
# unsafe_shutdowns +1 #
|
||||
if smart_second_last.unsafe_shutdowns - smart_last.unsafe_shutdowns >= 1:
|
||||
return ("Disk had {} unsafe shutdowns".format(smart_last.unsafe_shutdowns),
|
||||
"WARNING")
|
||||
|
||||
return ("", "OK")
|
||||
|
||||
|
||||
def create_app():
|
||||
|
||||
|
||||
db.create_all()
|
||||
config = {}
|
||||
|
||||
app.config["SECRET_KEY"] = secrets.token_urlsafe(64)
|
||||
|
||||
# load app config #
|
||||
config_dir = app.config["JSON_CONFIG_DIR"]
|
||||
main_config_file = "./{}/config.json".format(config_dir)
|
||||
print(main_config_file)
|
||||
if os.path.isfile(main_config_file):
|
||||
with open(main_config_file) as config_file:
|
||||
config_data = json.load(config_file)
|
||||
app.config |= config_data
|
||||
print(app.config)
|
||||
|
||||
if os.path.isfile(app.config["JSON_CONFIG_FILE"]):
|
||||
with open(app.config["JSON_CONFIG_FILE"]) as f:
|
||||
config |= json.load(f)
|
||||
elif os.path.isdir(app.config["JSON_CONFIG_DIR"]):
|
||||
for fname in os.listdir(app.config["JSON_CONFIG_DIR"]):
|
||||
fullpath = os.path.join(app.config["JSON_CONFIG_DIR"], fname)
|
||||
if fname.endswith(".json"):
|
||||
if fname.endswith(".json") and not fname == "config.json":
|
||||
with open(fullpath) as f:
|
||||
config |= json.load(f)
|
||||
|
||||
# create dummy host #
|
||||
icingatools.create_master_host(app)
|
||||
|
||||
if not config:
|
||||
print("No valid configuration found - need at least one service")
|
||||
return
|
||||
@@ -317,11 +453,17 @@ def create_app():
|
||||
for key in config:
|
||||
timeout = timeparse.timeparse(config[key]["timeout"])
|
||||
staticly_configured = True
|
||||
db.session.merge(Service(service=key, token=config[key]["token"],
|
||||
db.session.merge(Service(service=key, token=config[key]["token"],
|
||||
staticly_configured=staticly_configured, timeout=timeout,
|
||||
owner=config[key]["owner"]))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
||||
49
smarttools.py
Normal file
49
smarttools.py
Normal file
@@ -0,0 +1,49 @@
|
||||
def normalize(smart):
|
||||
'''Load different types of SMART outputs'''
|
||||
|
||||
ret = dict()
|
||||
ret.update({ "temperature" : 0 })
|
||||
ret.update({ "critical_warning" : 0 })
|
||||
ret.update({ "unsafe_shutdowns" : 0 })
|
||||
ret.update({ "power_cycles" : 0 })
|
||||
ret.update({ "power_on_hours" : 0 })
|
||||
ret.update({ "available_spare" : 100 })
|
||||
ret.update({ "wearleveling_count" : 100 })
|
||||
|
||||
if "ata_smart_attributes" in smart:
|
||||
|
||||
# get main table #
|
||||
table = smart["ata_smart_attributes"]["table"]
|
||||
|
||||
# temperatur #
|
||||
ret["temperature"] = smart["temperature"]["current"]
|
||||
|
||||
for el in table:
|
||||
|
||||
# look for relevant metrics #
|
||||
name = el["name"].lower()
|
||||
target_name = el["name"].lower() # name in return map
|
||||
|
||||
# handle value mapping #
|
||||
use_raw = False
|
||||
if name == "used_rsvd_blk_cnt_tot":
|
||||
target_name = "available_spare"
|
||||
elif name == "power_cylce_count":
|
||||
target_name = "power_cycles"
|
||||
use_raw = True
|
||||
elif name == "power_on_hours":
|
||||
target_name = "power_on_hours"
|
||||
use_raw = True
|
||||
|
||||
# check if metric should be recorded #
|
||||
if target_name in ret:
|
||||
|
||||
# set return dict #
|
||||
if use_raw:
|
||||
value = el["raw"]["value"]
|
||||
else:
|
||||
value = el["value"]
|
||||
|
||||
ret[target_name] = value
|
||||
|
||||
return ret
|
||||
@@ -81,6 +81,7 @@ body{
|
||||
}
|
||||
|
||||
.last-status{
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
box-shadow: 0px 0px 4px 3px rgba(0,0,0,0.5);
|
||||
background: linear-gradient(to top, #cfc6b054 0%, #cfcfcfc4 100%);
|
||||
@@ -154,6 +155,22 @@ body{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.smart-info{
|
||||
font-family: monospace;
|
||||
|
||||
padding-top: 2px;
|
||||
padding-top: 2px;
|
||||
padding-left: 5px;
|
||||
|
||||
padding-left: 5px;
|
||||
|
||||
color: black;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.box{
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
@@ -176,3 +193,11 @@ body{
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.hover:hover{
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
td{
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
</br>
|
||||
{{ form.timeout.label }} {{ form.timeout() }} </br>
|
||||
|
||||
</br>
|
||||
{{ form.special_type.label }} {{ form.special_type() }} </br>
|
||||
|
||||
{% if is_modification %}
|
||||
<input class="form-button mt-4" type="submit" value="Send Modification">
|
||||
{% else %}
|
||||
|
||||
@@ -3,7 +3,33 @@
|
||||
<body>
|
||||
{% include "navbar.html" %}
|
||||
<div class="container">
|
||||
<a href="{{ icinga_link }}">Icinga Link</a>
|
||||
|
||||
<script>
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
function submit_manual(){
|
||||
fetch("/report", {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
"service" : "{{ service.service }}",
|
||||
"token" : "{{ service.token }}",
|
||||
"status" : "OK",
|
||||
"info" : "Submitted from Web-Interface" })
|
||||
}).then( () => {
|
||||
sleep(1000).then( () => {
|
||||
window.location.reload()
|
||||
})
|
||||
}).catch( () => {
|
||||
alert("Submission failed, see console for details.")
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2 class="service-name">Service: {{ service.service }}</h2>
|
||||
{% if service.staticly_configured %}
|
||||
{% endif %}
|
||||
@@ -20,15 +46,23 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
{% else %}
|
||||
<p style="color: darkred;">No status for this service submitted</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ icinga_link }}" style="clear: both;" class="last-status hover mr-3">
|
||||
<p style="color: darkred;">Show in Icinga</p>
|
||||
</a>
|
||||
|
||||
<button style="clear: both;" class="last-status hover mr-3" onclick="submit_manual()">
|
||||
<p style="color: darkred;">Submit manual OK</p>
|
||||
</button>
|
||||
|
||||
<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>
|
||||
{% else %}
|
||||
<p style="color: darkred;">No status for this service submitted</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="clear p-4 box mt-4 mb-3" style="display: none;">
|
||||
<div class="service-timeout">Timeout: {{ service.timeout }} days</div>
|
||||
@@ -37,24 +71,71 @@
|
||||
class="service-token">Secret Token: {{ service.token }}</div>
|
||||
</div>
|
||||
|
||||
{% if service.special_type == "SMART" %}
|
||||
<div class="clear smart-info mt-3" style="background-color: orange;">
|
||||
Smart Monitor {% if smart %} for: {{ smart.model_number }} {% endif %}
|
||||
</div>
|
||||
<div class="clear smart-info mt-3" style="background-color: orange;">
|
||||
Example below requires smartmontools ("smartctl") in PATH.
|
||||
On Linux this is usually available via the package manager,
|
||||
on Windows install it from the
|
||||
<a style="text-decoration: underline; color: #5000e1; font-weight: bold;" href="https://www.smartmontools.org/wiki/Download#InstalltheWindowspackage">offical page</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if smart %}
|
||||
<h5 class="clear my-4">Linux</h5>
|
||||
{% else %}
|
||||
<h5 class="clear my-4">Curl</h5>
|
||||
{% endif %}
|
||||
<div class="ml-3 example">
|
||||
{% if smart %}
|
||||
SMART='{ <br>
|
||||
<div class="example-indent">
|
||||
"service" : "{{ service.service }}", <br>
|
||||
"token" : "{{ service.token }}", <br>
|
||||
"status" : "N/A", <br>
|
||||
"smart" : '$(/sbin/smartctl -a /dev/nvme0n1 --json)' <br>
|
||||
</div>
|
||||
}' <br><br>
|
||||
curl -X POST -H "Content-Type: application/json" \ <br>
|
||||
<div class="example-indent">
|
||||
--data "${SMART}" \ <br>
|
||||
{{ flask.request.url_root.replace("http://", "https://" )}}report
|
||||
</div>
|
||||
{% else %}
|
||||
curl -X POST \ <br>
|
||||
<div class="example-indent">
|
||||
-H "Content-Type: application/json" \ <br>
|
||||
-d '{ "service" : "{{ service.service }}",
|
||||
"token" : "{{ service.token }}", <br>
|
||||
"status" : "OK", "info" : "Free Text Information here" }' \<br>
|
||||
{{ flask.request.url_root }}
|
||||
{{ flask.request.url_root.replace("http://", "https://" )}}report
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if smart %}
|
||||
<h5 class="my-4">Windows</h5>
|
||||
<div class="ml-3 example">
|
||||
$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
|
||||
</div>
|
||||
{% else %}
|
||||
<h5 class="my-4">Python</h5>
|
||||
<div class="ml-3 example">
|
||||
import requests
|
||||
requests.post("{{ flask.request.url_root }}",<br>
|
||||
import requests<br>
|
||||
requests.post("{{ flask.request.url_root.replace("http://", "https://")}}report",
|
||||
<br>
|
||||
<div class="example-indent-double">
|
||||
json= { "service_name" : "{{ service.service }}", <br>
|
||||
json= { "service" : "{{ service.service }}", <br>
|
||||
<div class="example-indent-double">
|
||||
"token" : "{{ service.token }}", <br>
|
||||
"status" : "OK", </br>
|
||||
@@ -62,24 +143,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class="mb-4 mt-5 status-table">
|
||||
<thead>
|
||||
<th>Date</th>
|
||||
<th>Status</th>
|
||||
<th>Info</th>
|
||||
</thead>
|
||||
<table class="mb-4 mt-5 status-table">
|
||||
<thead>
|
||||
<th>Date</th>
|
||||
<th>Status</th>
|
||||
<th>Info</th>
|
||||
</thead>
|
||||
|
||||
<tbody class="mt-2">
|
||||
{% for status in status_list %}
|
||||
<tr>
|
||||
<tbody class="mt-2">
|
||||
{% for status in status_list %}
|
||||
<tr>
|
||||
<td>{{ status.human_date() }}</td>
|
||||
<td class="{{ status.status }}">{{ status.status }}</td>
|
||||
<td>{{ status.info_text }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<td class="{{ status.status }}">{{ status.status }}</td>
|
||||
<td>{{ status.info_text }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user