sysview-py 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sysview_py/__init__.py +0 -0
- sysview_py/db.py +122 -0
- sysview_py/flock.py +13 -0
- sysview_py/main.py +27 -0
- sysview_py/report.py +185 -0
- sysview_py/static/simple.css +779 -0
- sysview_py/static/sysview.css +38 -0
- sysview_py/sysview.py +114 -0
- sysview_py/templates/footer.html +5 -0
- sysview_py/templates/header.html +26 -0
- sysview_py/templates/host.html +19 -0
- sysview_py/templates/index.html +22 -0
- sysview_py/templates/index.html.table +22 -0
- sysview_py/templates/service.html +6 -0
- sysview_py/templates/services.html +22 -0
- sysview_py-0.1.0.dist-info/METADATA +10 -0
- sysview_py-0.1.0.dist-info/RECORD +19 -0
- sysview_py-0.1.0.dist-info/WHEEL +4 -0
- sysview_py-0.1.0.dist-info/entry_points.txt +3 -0
sysview_py/__init__.py
ADDED
|
File without changes
|
sysview_py/db.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import sqlite3
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SysviewDB:
|
|
6
|
+
schema = """
|
|
7
|
+
CREATE TABLE hosts(name, UNIQUE(name));
|
|
8
|
+
CREATE TABLE types(name, UNIQUE(name));
|
|
9
|
+
CREATE TABLE hosts_types(host, type, UNIQUE(host, type));
|
|
10
|
+
CREATE TABLE reports(hostname, type, date, raw_report);
|
|
11
|
+
CREATE TABLE items(command, status, status_text, additional_text, raw_item, report_id);
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, path):
|
|
14
|
+
self.path = path
|
|
15
|
+
self._connection = None
|
|
16
|
+
self._cursor = None
|
|
17
|
+
|
|
18
|
+
def connect(self):
|
|
19
|
+
db_exists = False
|
|
20
|
+
if self.path.exists():
|
|
21
|
+
db_exists = True
|
|
22
|
+
self._connection = sqlite3.connect(self.path)
|
|
23
|
+
self._cursor = self._connection.cursor()
|
|
24
|
+
if not db_exists:
|
|
25
|
+
self.init()
|
|
26
|
+
|
|
27
|
+
def init(self):
|
|
28
|
+
self._cursor.executescript(self.schema)
|
|
29
|
+
self._connection.commit()
|
|
30
|
+
|
|
31
|
+
def get_hosts(self):
|
|
32
|
+
hosts = []
|
|
33
|
+
hostnames = self._cursor.execute("SELECT name FROM hosts").fetchall()
|
|
34
|
+
hostnames = (host[0] for host in hostnames)
|
|
35
|
+
for hostname in hostnames:
|
|
36
|
+
if hostname is None:
|
|
37
|
+
continue
|
|
38
|
+
types = self.get_types(hostname)
|
|
39
|
+
hosts.append((hostname, types))
|
|
40
|
+
return hosts
|
|
41
|
+
|
|
42
|
+
def get_types(self, hostname):
|
|
43
|
+
sql = f"SELECT type FROM hosts_types WHERE host = '{hostname}'"
|
|
44
|
+
report_types = self._cursor.execute(sql).fetchall()
|
|
45
|
+
report_types = [report_type[0] for report_type in report_types]
|
|
46
|
+
return report_types
|
|
47
|
+
|
|
48
|
+
def get_latest_reports(self, hostname):
|
|
49
|
+
reports = []
|
|
50
|
+
host_types = self.get_types(hostname)
|
|
51
|
+
for host_type in host_types:
|
|
52
|
+
sql = f"SELECT rowid,raw_report from reports WHERE type = '{host_type}' AND hostname= '{hostname}' ORDER BY rowid DESC LIMIT 1"
|
|
53
|
+
latest_report = self._cursor.execute(sql).fetchone()
|
|
54
|
+
reports.append(latest_report)
|
|
55
|
+
return reports
|
|
56
|
+
|
|
57
|
+
def add(self, report):
|
|
58
|
+
report_data = (report.hostname, report.type, report.date, report.raw_report)
|
|
59
|
+
self._cursor.execute("INSERT INTO reports VALUES (?, ?, ?, ?)", report_data)
|
|
60
|
+
report_id = self._cursor.lastrowid
|
|
61
|
+
|
|
62
|
+
self._cursor.execute(
|
|
63
|
+
"INSERT OR IGNORE INTO hosts VALUES (?)", (report.hostname,)
|
|
64
|
+
)
|
|
65
|
+
self._cursor.execute("INSERT OR IGNORE INTO types VALUES (?)", (report.type,))
|
|
66
|
+
self._cursor.execute(
|
|
67
|
+
"INSERT OR IGNORE INTO hosts_types VALUES (?,?)",
|
|
68
|
+
(report.hostname, report.type),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
items = [
|
|
72
|
+
(
|
|
73
|
+
i.command,
|
|
74
|
+
i.status,
|
|
75
|
+
i.status_text,
|
|
76
|
+
i.additional_text,
|
|
77
|
+
i.raw_item,
|
|
78
|
+
report_id,
|
|
79
|
+
)
|
|
80
|
+
for i in report.items
|
|
81
|
+
]
|
|
82
|
+
self._cursor.executemany("INSERT INTO items VALUES(?, ?, ?, ?, ?, ?)", items)
|
|
83
|
+
|
|
84
|
+
self._connection.commit()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def adapt_date_iso(val):
|
|
88
|
+
"""Adapt datetime.date to ISO 8601 date."""
|
|
89
|
+
return val.isoformat()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def adapt_datetime_iso(val):
|
|
93
|
+
"""Adapt datetime.datetime to timezone-naive ISO 8601 date."""
|
|
94
|
+
return val.replace(tzinfo=None).isoformat()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def adapt_datetime_epoch(val):
|
|
98
|
+
"""Adapt datetime.datetime to Unix timestamp."""
|
|
99
|
+
return int(val.timestamp())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def convert_date(val):
|
|
103
|
+
"""Convert ISO 8601 date to datetime.date object."""
|
|
104
|
+
return datetime.date.fromisoformat(val.decode())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def convert_datetime(val):
|
|
108
|
+
"""Convert ISO 8601 datetime to datetime.datetime object."""
|
|
109
|
+
return datetime.datetime.fromisoformat(val.decode())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def convert_timestamp(val):
|
|
113
|
+
"""Convert Unix epoch timestamp to datetime.datetime object."""
|
|
114
|
+
return datetime.datetime.fromtimestamp(int(val))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
sqlite3.register_converter("date", convert_date)
|
|
118
|
+
sqlite3.register_converter("datetime", convert_datetime)
|
|
119
|
+
sqlite3.register_converter("timestamp", convert_timestamp)
|
|
120
|
+
sqlite3.register_adapter(datetime.date, adapt_date_iso)
|
|
121
|
+
sqlite3.register_adapter(datetime.datetime, adapt_datetime_iso)
|
|
122
|
+
sqlite3.register_adapter(datetime.datetime, adapt_datetime_epoch)
|
sysview_py/flock.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fcntl
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@contextmanager
|
|
6
|
+
def lock_file(file_path):
|
|
7
|
+
f = open(file_path, 'w')
|
|
8
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
9
|
+
try:
|
|
10
|
+
yield f
|
|
11
|
+
finally:
|
|
12
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
13
|
+
f.close()
|
sysview_py/main.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from .sysview import Sysview
|
|
5
|
+
from .flock import lock_file
|
|
6
|
+
|
|
7
|
+
LOCK_FILE=Path.home() / ".sysview.lock"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
with lock_file(LOCK_FILE):
|
|
12
|
+
sysview = Sysview()
|
|
13
|
+
db_path = Path.home() / ".sysview.db"
|
|
14
|
+
sysview.setup_db(db_path)
|
|
15
|
+
reports = []
|
|
16
|
+
if not sys.stdin.isatty():
|
|
17
|
+
reports = sysview.read_raw_reports_from_stdin()
|
|
18
|
+
if reports:
|
|
19
|
+
sysview.process_reports(reports)
|
|
20
|
+
sysview.read_hosts()
|
|
21
|
+
sysview.get_latest_reports()
|
|
22
|
+
sysview.update_html()
|
|
23
|
+
print("finished")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
main()
|
sysview_py/report.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
status_map = {
|
|
6
|
+
"UNKNOWN": 3,
|
|
7
|
+
"CRITICAL": 2,
|
|
8
|
+
"WARNING": 1,
|
|
9
|
+
"OK": 0
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class Host:
|
|
13
|
+
def __init__(self, name, report_types=tuple(), reports=[]):
|
|
14
|
+
self.name = name
|
|
15
|
+
self.report_types = report_types
|
|
16
|
+
self.reports = reports
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def last_updated(self):
|
|
20
|
+
last_updated = None
|
|
21
|
+
for report in self.reports:
|
|
22
|
+
if last_updated is None:
|
|
23
|
+
last_updated = report.date
|
|
24
|
+
elif report.date > last_updated:
|
|
25
|
+
last_updated = report.date
|
|
26
|
+
return last_updated
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def all_items(self):
|
|
30
|
+
items = []
|
|
31
|
+
for report in self.reports:
|
|
32
|
+
for item in report.items:
|
|
33
|
+
item.report_type = report.type
|
|
34
|
+
item.date = report.date
|
|
35
|
+
item.host = self.name
|
|
36
|
+
items += report.items
|
|
37
|
+
items.sort(key=lambda item: item.status_code, reverse=True)
|
|
38
|
+
return items
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def status(self):
|
|
42
|
+
status = "UNKNOWN"
|
|
43
|
+
report_status = [report.status for report in self.reports]
|
|
44
|
+
if "OK" in report_status:
|
|
45
|
+
status = "OK"
|
|
46
|
+
if "UNKNOWN" in report_status:
|
|
47
|
+
status = "UNKNOWN"
|
|
48
|
+
if "WARNING" in report_status:
|
|
49
|
+
status = "WARNING"
|
|
50
|
+
if "CRITICAL" in report_status:
|
|
51
|
+
status = "CRITICAL"
|
|
52
|
+
return status
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def status_code(self):
|
|
56
|
+
return status_map[self.status]
|
|
57
|
+
|
|
58
|
+
def __repr__(self):
|
|
59
|
+
return f"Host(name={self.name}, status={self.status}, report_types={self.report_types})"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ReportItem:
|
|
63
|
+
valid_statuses = ("OK", "WARNING", "CRITICAL", "UNKNOWN")
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
raw_item,
|
|
68
|
+
command=None,
|
|
69
|
+
status="UNKNOWN",
|
|
70
|
+
status_text=None,
|
|
71
|
+
additional_text=None,
|
|
72
|
+
date=None,
|
|
73
|
+
host=None,
|
|
74
|
+
report_type=None,
|
|
75
|
+
report_id=None
|
|
76
|
+
):
|
|
77
|
+
self.raw_item = raw_item
|
|
78
|
+
self.command = command
|
|
79
|
+
self._status = status
|
|
80
|
+
self.status_text = status_text
|
|
81
|
+
self.additional_text = additional_text
|
|
82
|
+
self.date = date
|
|
83
|
+
self.report_type = report_type
|
|
84
|
+
self.report_id = report_id
|
|
85
|
+
self.host = host
|
|
86
|
+
|
|
87
|
+
def __repr__(self):
|
|
88
|
+
return f"ReportItem(command={self.command}, status={self.status})"
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def status(self):
|
|
92
|
+
age = datetime.now() - self.date
|
|
93
|
+
status = self._status
|
|
94
|
+
if age > 1:
|
|
95
|
+
status = "OUTDATED"
|
|
96
|
+
return status
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def status_code(self):
|
|
100
|
+
return status_map[self.status]
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def id(self):
|
|
104
|
+
return hashlib.sha256((f"{self.report_id}_{self.command}").encode('utf-8')).hexdigest()
|
|
105
|
+
|
|
106
|
+
def parse(self):
|
|
107
|
+
parsed_required_fields = False
|
|
108
|
+
self.command = None
|
|
109
|
+
self.status = "UNKNOWN"
|
|
110
|
+
self.status_text = None
|
|
111
|
+
self.additional_text = None
|
|
112
|
+
for line in self.raw_item.split("\n"):
|
|
113
|
+
if None not in (self.command, self.status, self.status_text):
|
|
114
|
+
parsed_required_fields = True
|
|
115
|
+
if line.startswith("Command:") and not parsed_required_fields:
|
|
116
|
+
self.command = ":".join(line.split(":")[1:]).lstrip()
|
|
117
|
+
elif (
|
|
118
|
+
line.split(":")[0] in self.valid_statuses and not parsed_required_fields
|
|
119
|
+
):
|
|
120
|
+
self.status = line.split(":")[0]
|
|
121
|
+
self.status_text = ":".join(line.split(":")[1:])
|
|
122
|
+
elif parsed_required_fields:
|
|
123
|
+
if self.additional_text is None:
|
|
124
|
+
self.additional_text = ""
|
|
125
|
+
self.additional_text += f"{line}\n"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Report:
|
|
129
|
+
def __init__(
|
|
130
|
+
self, raw_report, hostname=None, date=None, report_type=None, items=None, report_id=None
|
|
131
|
+
):
|
|
132
|
+
self.raw_report = raw_report
|
|
133
|
+
self.hostname = hostname
|
|
134
|
+
self.date = date
|
|
135
|
+
self.type = report_type
|
|
136
|
+
self.items = items
|
|
137
|
+
self.report_id = report_id
|
|
138
|
+
|
|
139
|
+
def __repr__(self):
|
|
140
|
+
return f"Report(hostname={self.hostname}, type={self.type}, date={self.date}, status={self.status})"
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def status(self):
|
|
144
|
+
status = "UNKNOWN"
|
|
145
|
+
item_status = [item.status for item in self.items]
|
|
146
|
+
if "OK" in item_status:
|
|
147
|
+
status = "OK"
|
|
148
|
+
if "WARNING" in item_status:
|
|
149
|
+
status = "WARNING"
|
|
150
|
+
if "CRITICAL" in item_status:
|
|
151
|
+
status = "CRITICAL"
|
|
152
|
+
return status
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def status_code(self):
|
|
156
|
+
return status_map[self.status]
|
|
157
|
+
|
|
158
|
+
def parse(self):
|
|
159
|
+
item_pointer = 0
|
|
160
|
+
raw_report = []
|
|
161
|
+
self.items = []
|
|
162
|
+
raw_item = ""
|
|
163
|
+
for line in self.raw_report.split("\n"):
|
|
164
|
+
raw_report.append(line)
|
|
165
|
+
if line.startswith("Hostname:") and item_pointer == 0:
|
|
166
|
+
self.hostname = line.split(":")[1].strip()
|
|
167
|
+
elif line.startswith("Date:") and item_pointer == 0:
|
|
168
|
+
raw_date = line.replace("Date:", "").strip()
|
|
169
|
+
self.date = datetime.strptime(raw_date, "%a %b %d %X %Z %Y")
|
|
170
|
+
elif line.startswith("Type:") and item_pointer == 0:
|
|
171
|
+
self.type = "".join(line.split(":")[1].strip())
|
|
172
|
+
elif item_pointer > 0 and line != "---":
|
|
173
|
+
raw_item += f"{line}\n"
|
|
174
|
+
elif "---" == line:
|
|
175
|
+
if item_pointer > 0:
|
|
176
|
+
item = ReportItem(raw_item, report_id=self.report_id)
|
|
177
|
+
item.parse()
|
|
178
|
+
self.items.append(item)
|
|
179
|
+
item_pointer = item_pointer + 1
|
|
180
|
+
raw_item = ""
|
|
181
|
+
item = ReportItem(raw_item, report_id=self.report_id)
|
|
182
|
+
item.parse()
|
|
183
|
+
self.items.append(item)
|
|
184
|
+
self.items.sort(key=lambda item: item.status_code, reverse=True)
|
|
185
|
+
self.raw_report = "\n".join(raw_report)
|