40 Commits

Author SHA1 Message Date
d5d964ea8e wip: 2024-01-03 13:40:42 +01:00
8d4882d7c4 wip: 2024-01-03 12:39:16 +01:00
6ca2278838 wip: 2024-01-03 12:25:01 +01:00
8884e924e8 wip: 2024-01-03 12:24:54 +01:00
9eef4b1353 wip: 2024-01-03 01:21:34 +01:00
ebb644fdf2 wip: 2024-01-03 00:48:44 +01:00
c487c769b0 wip: 2024-01-02 13:54:04 +01:00
ff2d8296f3 wip: 2024-01-02 12:31:06 +01:00
ad08f69df1 wip: 2024-01-01 20:41:29 +01:00
1860cba273 feat: switch to alpine image 2023-12-30 13:22:17 +01:00
95db2f4b9f fix: healthcheck alive url 2023-12-30 01:05:39 +01:00
5439f46bd3 fix: update image & set registry path 2023-12-30 00:54:42 +01:00
163854d7bd fix: improve layout & manual callback 2023-11-12 08:38:43 +01:00
c1addbf9fb fix: timestamp status name 2023-11-12 08:21:11 +01:00
3b89f534dd feat: implement manual submission button 2023-11-12 08:09:41 +01:00
6c1afa82ff fix: limit and oder status results displayed 2023-11-12 08:03:00 +01:00
cfab1e499c fix: python example [serivce_name->service] 2023-11-12 07:46:49 +01:00
fed1eab6d0 fix: better modification form check 2023-07-25 00:57:30 +02:00
8be2e3d054 fix: remove double POST-method check 2023-07-25 00:51:13 +02:00
c93ab82119 fix: prevent double creation of icinga services on modify 2023-07-25 00:46:45 +02:00
5b49bb2b37 fix: create call on modify 2023-07-25 00:35:01 +02:00
c4ed500961 update: cr year 2023-07-21 15:23:57 +02:00
9ac05997d3 fix: correctly display timeout as days on modify 2023-07-15 10:48:50 +02:00
fb1356bbb4 fix: missing <br> after import example 2023-07-09 15:44:28 +02:00
ef6b3fa5fb fix: day <> seconds unit mistake 2023-07-08 23:43:36 +02:00
0fb170c97e fix: remove obsolete todo 2023-07-08 16:02:02 +02:00
3ba958f6e5 fix: use fallback filter icinga link for static services 2023-07-08 16:01:50 +02:00
2d221a38c6 feat: add owner variable for later filter 2023-07-08 15:46:28 +02:00
696f190667 fix: use https by default 2023-07-08 03:15:19 +02:00
5449fd1437 update: use new report location 2023-07-08 03:06:35 +02:00
e086a221b7 fix: td width for very large outputs 2023-07-08 03:01:02 +02:00
7ea5578132 change: new reporting location 2023-07-08 02:53:17 +02:00
bb1c2a25e3 fix: improve icinga link 2023-07-08 02:52:23 +02:00
a726032cf6 update: css hover/inline block 2023-07-08 02:51:57 +02:00
8c39628a51 fix: handle none user better 2023-07-07 13:54:34 +02:00
876e3101c0 fix: ignore config.json for service config 2023-07-07 13:44:48 +02:00
0d6e2aef7b change: create host last in create_app 2023-07-07 13:44:07 +02:00
b879bd0a84 add: debug output 2023-07-07 13:31:11 +02:00
4653b5b425 feat: app config support 2023-07-07 13:19:20 +02:00
fcb410e947 fix: typo 2023-07-07 12:57:46 +02:00
11 changed files with 392 additions and 76 deletions

View File

@@ -34,4 +34,4 @@ jobs:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: "${{ secrets.REGISTRY }}/athq/async-icinga:latest" tags: "${{ secrets.REGISTRY }}/atlantishq/async-icinga:latest"

View File

@@ -1,25 +1,20 @@
FROM python:3.9-slim-buster FROM alpine
RUN apk add --no-cache py3-pip
RUN apt update RUN apk add --no-cache curl
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
WORKDIR /app WORKDIR /app
RUN python3 -m pip install waitress RUN python3 -m pip install --no-cache-dir --break-system-packages waitress
COPY req.txt . 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) # precreate database directory for mount (will otherwise be created at before_first_request)
COPY ./ . COPY ./ .
RUN mkdir /app/instance/ RUN mkdir /app/instance/
HEALTHCHECK --interval=1m --timeout=5s --start-period=10s \ 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 EXPOSE 5000/tcp
ENTRYPOINT ["waitress-serve"] ENTRYPOINT ["waitress-serve"]

View File

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

View File

@@ -21,9 +21,9 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
urlBase = "{proto}://{host}:{port}/?service={service}" urlBase = "{proto}://{host}:{port}/report?service={service}"
if not args.port: if not args.port:
urlBase = "{proto}://{host}/?service={service}" urlBase = "{proto}://{host}/report?service={service}"
url = urlBase.format(proto=args.protocol, host=args.host, url = urlBase.format(proto=args.protocol, host=args.host,
port=args.port, service=args.service) port=args.port, service=args.service)

View File

@@ -41,6 +41,9 @@ def _build_service_name(user, async_service_name):
def create_service(user, async_service_name, app): def create_service(user, async_service_name, app):
if not app.config.get("ICINGA_API_URL"):
return
client = _create_client(app) client = _create_client(app)
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"]
@@ -54,7 +57,8 @@ def create_service(user, async_service_name, app):
"vars" : { "vars" : {
"host" : "async-icinga.atlantishq.de", "host" : "async-icinga.atlantishq.de",
"service_name" : async_service_name, "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): def delete_service(user, async_service_name, app):
if not app.config.get("ICINGA_API_URL"):
return
client = _create_client(app) client = _create_client(app)
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"]
@@ -73,11 +80,24 @@ def delete_service(user, async_service_name, app):
client.objects.delete("Service", service_api_helper_name) 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) name = _build_service_name(user, service_name)
url_fmt = "{base}/icingaweb2/dashboard/#!/icingaweb2/monitoring/service/show?host={host}&service={service}" 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) service=name)

View File

@@ -1,7 +1,7 @@
pytimeparse pytimeparse
flask flask
flask-sqlalchemy flask-sqlalchemy
flasl-wtf flask-wtf
waitress waitress
requests requests
icinga2api icinga2api

196
server.py
View File

@@ -12,7 +12,7 @@ import secrets
import flask_wtf import flask_wtf
from flask_wtf import FlaskForm 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 wtforms.validators import DataRequired, Length
from sqlalchemy import Column, Integer, String, Boolean, or_, and_ from sqlalchemy import Column, Integer, String, Boolean, or_, and_
@@ -24,6 +24,7 @@ from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
import icingatools import icingatools
import smarttools
app = flask.Flask("Icinga Report In Gateway") app = flask.Flask("Icinga Report In Gateway")
@@ -41,6 +42,7 @@ class Service(db.Model):
token = Column(String) token = Column(String)
timeout = Column(Integer) timeout = Column(Integer)
owner = Column(String) owner = Column(String)
special_type = Column(String)
staticly_configured = Column(Boolean) staticly_configured = Column(Boolean)
@@ -57,6 +59,22 @@ class Status(db.Model):
dt = datetime.datetime.fromtimestamp(self.timestamp) 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):
__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): def buildReponseDict(status, service=None):
if not status: if not status:
@@ -108,19 +126,34 @@ class EntryForm(FlaskForm):
service = StringField("Service Name") service = StringField("Service Name")
service_hidden = HiddenField("service_hidden") service_hidden = HiddenField("service_hidden")
special_type = SelectField("Type", choices=["Default", "SMART"])
timeout = DecimalField("Timeout in days", default=30) timeout = DecimalField("Timeout in days", default=30)
def create_entry(form, user): def create_entry(form, user):
# TODO add entry to icinga
token = secrets.token_urlsafe(16) token = secrets.token_urlsafe(16)
service_name = form.service.data or form.service_hidden.data service_name = form.service.data or form.service_hidden.data
service = Service(service=service_name, timeout=int(form.timeout.data), # keep token if modification #
owner=user, token=token) 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.merge(service)
db.session.commit() db.session.commit()
@@ -128,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("X-Forwarded-Preferred-Username") 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 #
@@ -137,15 +170,20 @@ def service_details():
# validate # # validate #
if not service: if not service:
return ("{} not found".format("service"), 404) 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)) 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, 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"]) @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")) 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")
if operation and operation == "delete" : if operation and operation == "delete" :
service_delete_name = flask.request.args.get("service") service_delete_name = flask.request.args.get("service")
@@ -171,19 +209,20 @@ def create_interface():
return flask.redirect("/overview") return flask.redirect("/overview")
form = EntryForm() form = EntryForm()
# handle modification # # handle modification #
modify_service_name = flask.request.args.get("service") modify_service_name = flask.request.args.get("service")
if modify_service_name: if modify_service_name:
service = db.session.query(Service).filter(Service.service == modify_service_name).first() service = db.session.query(Service).filter(Service.service == modify_service_name).first()
if service and service.owner == user: if service and service.owner == user:
form.service.default = service.service 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.service_hidden.default = service.service
form.process() form.process()
else: else:
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) create_entry(form, user)
service_name = form.service.data or form.service_hidden.data service_name = form.service.data or form.service_hidden.data
@@ -203,6 +242,7 @@ def reload():
return ("", 204) return ("", 204)
@app.route('/', methods=["GET", "POST"]) @app.route('/', methods=["GET", "POST"])
@app.route('/report', methods=["GET", "POST"])
def default(): def default():
if flask.request.method == "GET": if flask.request.method == "GET":
@@ -247,7 +287,7 @@ def default():
if not lastSuccess.timestamp == 0 and delta > timeout and latestInfoIsSuccess: if not lastSuccess.timestamp == 0 and delta > timeout and latestInfoIsSuccess:
# lastes info is success but timed out # # 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): if timeout/delta > 0.9 or (delta - timeout) < datetime.timedelta(hours=12):
lastSuccess.status = "WARNING" lastSuccess.status = "WARNING"
else: else:
@@ -265,16 +305,27 @@ def default():
elif flask.request.method == "POST": elif flask.request.method == "POST":
# get variables # # get variables #
service = flask.request.json["service"] service = flask.request.json.get("service")
token = flask.request.json["token"] token = flask.request.json.get("token")
status = flask.request.json["status"] status = flask.request.json.get("status")
text = flask.request.json["info"] text = flask.request.json.get("info") or "no_info"
timestamp = datetime.datetime.now().timestamp() 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: if not service:
return ("'service' ist empty field in json", 400) return ("'service' ist empty field in json", 400)
elif not token: elif not token:
return ("'token' ist empty field in json", 400) 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 # # verify token & service in config #
verifiedServiceObj = db.session.query(Service).filter( verifiedServiceObj = db.session.query(Service).filter(
@@ -283,33 +334,118 @@ def default():
if not verifiedServiceObj: if not verifiedServiceObj:
return ("Service ({}) with this token ({}) not found in DB".format(service, token), 401) return ("Service ({}) with this token ({}) not found in DB".format(service, token), 401)
else: 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.merge(status)
db.session.commit() db.session.commit()
return ("", 204) return ("", 204)
else: else:
return ("Method not implemented: {}".format(flask.request.method), 405) 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(): def create_app():
db.create_all() db.create_all()
config = {} config = {}
app.config["SECRET_KEY"] = secrets.token_urlsafe(64) 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"]): if os.path.isfile(app.config["JSON_CONFIG_FILE"]):
with open(app.config["JSON_CONFIG_FILE"]) as f: with open(app.config["JSON_CONFIG_FILE"]) as f:
config |= json.load(f) config |= json.load(f)
elif os.path.isdir(app.config["JSON_CONFIG_DIR"]): elif os.path.isdir(app.config["JSON_CONFIG_DIR"]):
for fname in os.listdir(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) 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: with open(fullpath) as f:
config |= json.load(f) config |= json.load(f)
# create dummy host #
icingatools.create_master_host(app)
if not config: if not config:
print("No valid configuration found - need at least one service") print("No valid configuration found - need at least one service")
return return
@@ -317,11 +453,17 @@ def create_app():
for key in config: for key in config:
timeout = timeparse.timeparse(config[key]["timeout"]) timeout = timeparse.timeparse(config[key]["timeout"])
staticly_configured = True 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, staticly_configured=staticly_configured, timeout=timeout,
owner=config[key]["owner"])) owner=config[key]["owner"]))
db.session.commit() 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__": if __name__ == "__main__":

49
smarttools.py Normal file
View 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

View File

@@ -81,6 +81,7 @@ body{
} }
.last-status{ .last-status{
display: inline-block;
margin-top: 10px; margin-top: 10px;
box-shadow: 0px 0px 4px 3px rgba(0,0,0,0.5); box-shadow: 0px 0px 4px 3px rgba(0,0,0,0.5);
background: linear-gradient(to top, #cfc6b054 0%, #cfcfcfc4 100%); background: linear-gradient(to top, #cfc6b054 0%, #cfcfcfc4 100%);
@@ -154,6 +155,22 @@ body{
cursor: pointer; 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{ .box{
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
@@ -176,3 +193,11 @@ body{
.clear { .clear {
clear: both; clear: both;
} }
.hover:hover{
opacity: 0.7;
}
td{
max-width: 500px;
}

View File

@@ -37,6 +37,9 @@
</br> </br>
{{ form.timeout.label }} {{ form.timeout() }} </br> {{ form.timeout.label }} {{ form.timeout() }} </br>
</br>
{{ form.special_type.label }} {{ form.special_type() }} </br>
{% if is_modification %} {% if is_modification %}
<input class="form-button mt-4" type="submit" value="Send Modification"> <input class="form-button mt-4" type="submit" value="Send Modification">
{% else %} {% else %}

View File

@@ -3,7 +3,33 @@
<body> <body>
{% include "navbar.html" %} {% include "navbar.html" %}
<div class="container"> <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> <h2 class="service-name">Service: {{ service.service }}</h2>
{% if service.staticly_configured %} {% if service.staticly_configured %}
{% endif %} {% endif %}
@@ -20,15 +46,23 @@
</div> </div>
{% endif %} {% endif %}
<div class="last-status"> <a href="{{ icinga_link }}" style="clear: both;" class="last-status hover mr-3">
{% if status_list | length > 0 %} <p style="color: darkred;">Show in Icinga</p>
<p class="{{ status_list[0].status }}"> </a>
{{ status_list[0].status }} submitted on {{ status_list[0].human_date() }}
</p> <button style="clear: both;" class="last-status hover mr-3" onclick="submit_manual()">
{% else %} <p style="color: darkred;">Submit manual OK</p>
<p style="color: darkred;">No status for this service submitted</p> </button>
{% endif %}
</div> <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="clear p-4 box mt-4 mb-3" style="display: none;">
<div class="service-timeout">Timeout: {{ service.timeout }} days</div> <div class="service-timeout">Timeout: {{ service.timeout }} days</div>
@@ -37,24 +71,71 @@
class="service-token">Secret Token: {{ service.token }}</div> class="service-token">Secret Token: {{ service.token }}</div>
</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> <h5 class="clear my-4">Curl</h5>
{% endif %}
<div class="ml-3 example"> <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> curl -X POST \ <br>
<div class="example-indent"> <div class="example-indent">
-H "Content-Type: application/json" \ <br> -H "Content-Type: application/json" \ <br>
-d '{ "service" : "{{ service.service }}", -d '{ "service" : "{{ service.service }}",
"token" : "{{ service.token }}", <br> "token" : "{{ service.token }}", <br>
"status" : "OK", "info" : "Free Text Information here" }' \<br> "status" : "OK", "info" : "Free Text Information here" }' \<br>
{{ flask.request.url_root }} {{ flask.request.url_root.replace("http://", "https://" )}}report
</div> </div>
{% endif %}
</div> </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> <h5 class="my-4">Python</h5>
<div class="ml-3 example"> <div class="ml-3 example">
import requests import requests<br>
requests.post("{{ flask.request.url_root }}",<br> requests.post("{{ flask.request.url_root.replace("http://", "https://")}}report",
<br>
<div class="example-indent-double"> <div class="example-indent-double">
json= { "service_name" : "{{ service.service }}", <br> json= { "service" : "{{ service.service }}", <br>
<div class="example-indent-double"> <div class="example-indent-double">
"token" : "{{ service.token }}", <br> "token" : "{{ service.token }}", <br>
"status" : "OK", </br> "status" : "OK", </br>
@@ -62,24 +143,25 @@
</div> </div>
</div> </div>
</div> </div>
{% endif %}
<table class="mb-4 mt-5 status-table"> <table class="mb-4 mt-5 status-table">
<thead> <thead>
<th>Date</th> <th>Date</th>
<th>Status</th> <th>Status</th>
<th>Info</th> <th>Info</th>
</thead> </thead>
<tbody class="mt-2"> <tbody class="mt-2">
{% for status in status_list %} {% for status in status_list %}
<tr> <tr>
<td>{{ status.human_date() }}</td> <td>{{ status.human_date() }}</td>
<td class="{{ status.status }}">{{ status.status }}</td> <td class="{{ status.status }}">{{ status.status }}</td>
<td>{{ status.info_text }}</td> <td>{{ status.info_text }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</body> </body>
</html> </html>