This commit is contained in:
Mark Gregory
2026-10-22 09:27:17 +01:00
parent ed5bf665b9
commit 0a6f6ac058
22 changed files with 791 additions and 68 deletions
@@ -7,7 +7,7 @@
"key": "ALL SERVERS: %lld OK · %lld WARN · %lld FAIL",
"location": {
"startingColumn": 18,
"startingLine": 139
"startingLine": 289
}
},
{
@@ -15,7 +15,7 @@
"key": "%lld offline",
"location": {
"startingColumn": 26,
"startingLine": 17
"startingLine": 38
}
},
{
@@ -23,7 +23,7 @@
"key": "PEGASUS MONITOR",
"location": {
"startingColumn": 27,
"startingLine": 11
"startingLine": 32
}
},
{
@@ -31,7 +31,7 @@
"key": "SERVERS",
"location": {
"startingColumn": 22,
"startingLine": 22
"startingLine": 43
}
},
{
@@ -39,7 +39,7 @@
"key": "Switch Server",
"location": {
"startingColumn": 27,
"startingLine": 41
"startingLine": 62
}
},
{
@@ -47,7 +47,7 @@
"key": "%lld OK",
"location": {
"startingColumn": 46,
"startingLine": 64
"startingLine": 85
}
},
{
@@ -55,7 +55,31 @@
"key": "Up %@",
"location": {
"startingColumn": 50,
"startingLine": 66
"startingLine": 87
}
},
{
"comment": "",
"key": "Show Details",
"location": {
"startingColumn": 36,
"startingLine": 97
}
},
{
"comment": "",
"key": "Copy Error Details",
"location": {
"startingColumn": 36,
"startingLine": 135
}
},
{
"comment": "",
"key": "Refresh Server",
"location": {
"startingColumn": 36,
"startingLine": 154
}
},
{
@@ -63,7 +87,7 @@
"key": "SERVICES",
"location": {
"startingColumn": 38,
"startingLine": 101
"startingLine": 187
}
},
{
@@ -71,7 +95,7 @@
"key": "HEALTH GROUPS",
"location": {
"startingColumn": 38,
"startingLine": 117
"startingLine": 217
}
},
{
@@ -79,7 +103,7 @@
"key": "Uptime",
"location": {
"startingColumn": 38,
"startingLine": 92
"startingLine": 178
}
},
{
@@ -87,7 +111,39 @@
"key": "REQUIRED",
"location": {
"startingColumn": 50,
"startingLine": 110
"startingLine": 196
}
},
{
"comment": "",
"key": "Show Details",
"location": {
"startingColumn": 48,
"startingLine": 203
}
},
{
"comment": "",
"key": "Copy Details",
"location": {
"startingColumn": 48,
"startingLine": 211
}
},
{
"comment": "",
"key": "Show Details",
"location": {
"startingColumn": 44,
"startingLine": 232
}
},
{
"comment": "",
"key": "Copy Details",
"location": {
"startingColumn": 44,
"startingLine": 270
}
},
{
@@ -95,7 +151,7 @@
"key": "Refresh All",
"location": {
"startingColumn": 24,
"startingLine": 147
"startingLine": 297
}
},
{
@@ -103,7 +159,7 @@
"key": "Diagnostics",
"location": {
"startingColumn": 24,
"startingLine": 149
"startingLine": 299
}
},
{
@@ -111,7 +167,7 @@
"key": "Settings",
"location": {
"startingColumn": 24,
"startingLine": 150
"startingLine": 300
}
},
{
@@ -119,7 +175,7 @@
"key": "Quit",
"location": {
"startingColumn": 24,
"startingLine": 151
"startingLine": 301
}
},
{
@@ -127,7 +183,7 @@
"key": "%@ — Diagnostics",
"location": {
"startingColumn": 18,
"startingLine": 161
"startingLine": 317
}
},
{
@@ -135,7 +191,39 @@
"key": "Select a server in the menu bar to change this report.",
"location": {
"startingColumn": 18,
"startingLine": 162
"startingLine": 318
}
},
{
"comment": "",
"key": "Show Details",
"location": {
"startingColumn": 36,
"startingLine": 332
}
},
{
"comment": "",
"key": "Copy Details",
"location": {
"startingColumn": 36,
"startingLine": 344
}
},
{
"comment": "",
"key": "Copy Details",
"location": {
"startingColumn": 24,
"startingLine": 418
}
},
{
"comment": "",
"key": "Close",
"location": {
"startingColumn": 24,
"startingLine": 428
}
}
]
@@ -8,3 +8,4 @@ ae0397526b5e3c6e8848cdfd6a483288
7e38a5d8e664e612ef350f00745dc212
0339bfa5899b58e708b1bdc579f1e095
fc9e0d46560e0e3f6c4e552bd51875d2
aa90f81d02489c13e5c788a9f2cc7c59
@@ -6,6 +6,48 @@
<integer>13</integer>
<key>logs</key>
<dict>
<key>17BB5E1E-C7B0-4A22-A2E4-7B3343B7C0A0</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>17BB5E1E-C7B0-4A22-A2E4-7B3343B7C0A0.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
<dict>
<key>highLevelStatus</key>
<string>S</string>
<key>totalNumberOfAnalyzerIssues</key>
<integer>0</integer>
<key>totalNumberOfErrors</key>
<integer>0</integer>
<key>totalNumberOfTestFailures</key>
<integer>0</integer>
<key>totalNumberOfWarnings</key>
<integer>0</integer>
</dict>
<key>schemeIdentifier-containerName</key>
<string>PegasusMonitor project</string>
<key>schemeIdentifier-schemeName</key>
<string>PegasusMonitor</string>
<key>schemeIdentifier-sharedScheme</key>
<integer>1</integer>
<key>signature</key>
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
<key>timeStartedRecording</key>
<real>814350397.61442196</real>
<key>timeStoppedRecording</key>
<real>814350402.64668095</real>
<key>title</key>
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
<key>uniqueIdentifier</key>
<string>17BB5E1E-C7B0-4A22-A2E4-7B3343B7C0A0</string>
</dict>
<key>B61B7A1E-4BB1-405D-9BB7-4318215BFC0D</key>
<dict>
<key>className</key>
@@ -48,48 +90,6 @@
<key>uniqueIdentifier</key>
<string>B61B7A1E-4BB1-405D-9BB7-4318215BFC0D</string>
</dict>
<key>FEA45B81-12F7-48C7-BB4F-0CA06A4FC9A8</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>FEA45B81-12F7-48C7-BB4F-0CA06A4FC9A8.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
<dict>
<key>highLevelStatus</key>
<string>W</string>
<key>totalNumberOfAnalyzerIssues</key>
<integer>0</integer>
<key>totalNumberOfErrors</key>
<integer>0</integer>
<key>totalNumberOfTestFailures</key>
<integer>0</integer>
<key>totalNumberOfWarnings</key>
<integer>2</integer>
</dict>
<key>schemeIdentifier-containerName</key>
<string>PegasusMonitor project</string>
<key>schemeIdentifier-schemeName</key>
<string>PegasusMonitor</string>
<key>schemeIdentifier-sharedScheme</key>
<integer>1</integer>
<key>signature</key>
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
<key>timeStartedRecording</key>
<real>814283652.52319503</real>
<key>timeStoppedRecording</key>
<real>814283659.16746604</real>
<key>title</key>
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
<key>uniqueIdentifier</key>
<string>FEA45B81-12F7-48C7-BB4F-0CA06A4FC9A8</string>
</dict>
</dict>
</dict>
</plist>
@@ -1 +1 @@
1792592179.2212691: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/ModuleCache.noindex")
1792657599.394216: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/ModuleCache.noindex")
+1 -1
View File
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>LastAccessedDate</key>
<date>2026-10-21T14:16:17Z</date>
<date>2026-10-22T08:26:37Z</date>
<key>WorkspacePath</key>
<string>/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor.xcodeproj</string>
</dict>
+284 -4
View File
@@ -3,6 +3,27 @@ import SwiftUI
struct MonitorMenuView: View {
@ObservedObject var monitor: ServerMonitor
@Environment(\.openWindow) private var openWindow
@State private var detailItem: MonitorDetailItem?
private func showDetails(
title: String,
status: String,
details: String
) {
detailItem = MonitorDetailItem(
title: title,
status: status,
details: details
)
}
private func copyDetails(_ details: String) {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(
details,
forType: .string
)
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
@@ -70,7 +91,72 @@ struct MonitorMenuView: View {
if server.id == monitor.selectedID { Image(systemName: "checkmark") }
}.padding(.horizontal, 14).padding(.vertical, 8)
.contentShape(Rectangle())
}.buttonStyle(.plain)
}
.buttonStyle(.plain)
.contextMenu {
Button("Show Details") {
let result = monitor.results[server.id]
let report = result?.report
let failures = report?.checks.filter {
$0.status != .ok
} ?? []
let checkDetails = failures.map {
"\($0.section) / \($0.name) [\($0.status.title)]\\n\($0.detail)"
}.joined(separator: "\\n\\n")
let missingGroups = report?.groups
.filter { $0.status != .ok }
.map {
"\($0.name): \($0.missing.joined(separator: ", "))"
}
.joined(separator: "\\n") ?? ""
let details = [
"Server: \(server.name)",
"Host: \(server.host)",
"Status: \(result?.status.title ?? "Unknown")",
result?.error ?? "",
missingGroups,
checkDetails,
report?.diagnostics ?? ""
]
.filter { !$0.isEmpty }
.joined(separator: "\\n\\n")
showDetails(
title: server.name,
status: result?.status.title ?? "Unknown",
details: details
)
}
Button("Copy Error Details") {
let result = monitor.results[server.id]
let failures = result?.report?.checks
.filter { $0.status != .ok }
.map {
"\($0.name): \($0.detail)"
}
.joined(separator: "\\n") ?? ""
copyDetails(
result?.error
?? (failures.isEmpty
? "No errors reported."
: failures)
)
}
Divider()
Button("Refresh Server") {
Task {
await monitor.refresh(id: server.id)
}
}
}
}
}
}
@@ -112,6 +198,20 @@ struct MonitorMenuView: View {
Text(service.title).font(.caption).foregroundStyle(.secondary)
}
.padding(.horizontal, 14).padding(.vertical, 5)
.contentShape(Rectangle())
.contextMenu {
Button("Show Details") {
showDetails(
title: service.name,
status: service.title,
details: service.detail
)
}
Button("Copy Details") {
copyDetails(service.detail)
}
}
}
Divider()
Text("HEALTH GROUPS")
@@ -124,7 +224,57 @@ struct MonitorMenuView: View {
Spacer()
Label(group.status.title, systemImage: group.status.symbol)
.foregroundStyle(group.status.color).font(.caption)
}.padding(.horizontal, 14).padding(.vertical, 8)
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
.contentShape(Rectangle())
.contextMenu {
Button("Show Details") {
let related = report.checks.filter {
group.missing.contains($0.name)
|| (
$0.status != .ok
&& $0.section == group.name
)
}
let messages = related.map {
"\($0.name): \($0.detail)"
}.joined(separator: "\\n\\n")
let missing = group.missing.isEmpty
? ""
: "Missing: "
+ group.missing.joined(separator: ", ")
let details = [
"Group: \(group.name)",
"Status: \(group.status.title)",
missing,
messages.isEmpty
? (group.status == .ok
? "All required checks passed."
: "No matching individual check details were returned.")
: messages
]
.filter { !$0.isEmpty }
.joined(separator: "\\n\\n")
showDetails(
title: group.name,
status: group.status.title,
details: details
)
}
Button("Copy Details") {
copyDetails(
"\(group.name): \(group.status.title)\\n"
+ group.missing.joined(separator: "\\n")
)
}
}
Divider().padding(.leading, 14)
}
} else {
@@ -150,12 +300,18 @@ struct MonitorMenuView: View {
Button("Settings") { openWindow(id: "monitor-settings") }
Button("Quit") { NSApp.terminate(nil) }
}.buttonStyle(.borderless).padding(14)
}.frame(width: 410)
}
.frame(width: 410)
.sheet(item: $detailItem) { item in
MonitorDetailSheet(item: item)
}
}
}
struct DiagnosticsView: View {
@ObservedObject var monitor: ServerMonitor
@State private var detailItem: MonitorDetailItem?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("\(monitor.selected?.name ?? "Server") — Diagnostics").font(.title2.bold())
@@ -171,9 +327,133 @@ struct DiagnosticsView: View {
}
Spacer()
}
.contentShape(Rectangle())
.contextMenu {
Button("Show Details") {
detailItem = MonitorDetailItem(
title: check.name,
status: check.status.title,
details: """
Section: \(check.section)
\(check.detail)
"""
)
}
Button("Copy Details") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(
check.detail,
forType: .string
)
}
}
}
}
}.textSelection(.enabled)
}.padding(20).frame(minWidth: 650, minHeight: 450)
}
.padding(20)
.frame(minWidth: 650, minHeight: 450)
.sheet(item: $detailItem) { item in
MonitorDetailSheet(item: item)
}
}
}
// MARK: - Monitoring Details
struct MonitorDetailItem: Identifiable {
let id = UUID()
let title: String
let status: String
let details: String
}
struct MonitorDetailSheet: View {
let item: MonitorDetailItem
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: icon)
.font(.title2)
.foregroundStyle(statusColor)
VStack(alignment: .leading, spacing: 4) {
Text(item.title)
.font(.title2.bold())
Text(item.status)
.foregroundStyle(statusColor)
}
Spacer()
}
Divider()
ScrollView {
Text(item.details.isEmpty
? "No additional diagnostic information available."
: item.details)
.font(.system(
size: 12,
design: .monospaced
))
.frame(
maxWidth: .infinity,
alignment: .leading
)
.textSelection(.enabled)
.padding(12)
}
.background(.quaternary.opacity(0.3))
.clipShape(RoundedRectangle(cornerRadius: 8))
HStack {
Button("Copy Details") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(
item.details,
forType: .string
)
}
Spacer()
Button("Close") {
dismiss()
}
.keyboardShortcut(.defaultAction)
}
}
.padding(20)
.frame(minWidth: 540, minHeight: 350)
}
private var icon: String {
switch item.status.lowercased() {
case "critical", "failed":
return "xmark.circle.fill"
case "warning", "stopped":
return "exclamationmark.triangle.fill"
default:
return "checkmark.circle.fill"
}
}
private var statusColor: Color {
switch item.status.lowercased() {
case "critical", "failed":
return .red
case "warning", "stopped":
return .orange
default:
return .green
}
}
}
+25 -1
View File
@@ -49,6 +49,25 @@ def output():
match = re.match(r'^\[(OK\s*|WARN|FAIL)\]\s+(.+?)(?:\s+—\s+(.*))?$', line)
if match:
checks.append({'name': match[2].strip(), 'status': match[1].strip().lower(), 'detail': (match[3] or '').strip(), 'section': section})
# Noncritical firmware metadata failures are warnings.
# Other failed systemd units remain critical.
for item in checks:
if item['name'] != 'Failed systemd units':
continue
if item['status'] != 'fail':
continue
failed_units = re.findall(
r'([A-Za-z0-9@_.:-]+\\.service)\\s+loaded\\s+failed\\s+failed',
item['detail']
)
if failed_units and all(
unit == 'fwupd-refresh.service'
for unit in failed_units
):
item['status'] = 'warn'
lookup = {c['name']: c['status'] for c in checks}
groups = []
for title, names in GROUPS:
@@ -62,7 +81,12 @@ def output():
groups.append({'name':label, 'status':state, 'missing':[n for n in relevant if n not in lookup]})
counts_match = re.search(r'SUMMARY:\s*(\d+) OK\s*\|\s*(\d+) WARN\s*\|\s*(\d+) FAIL', plain)
counts = dict(zip(('ok','warn','fail'), (int(x) for x in counts_match.groups()))) if counts_match else {'ok':0,'warn':0,'fail':1}
if not checks:
if checks:
counts = {
state: sum(c['status'] == state for c in checks)
for state in ('ok', 'warn', 'fail')
}
else:
counts['fail'] = max(1, counts['fail'])
overall = 'fail' if counts['fail'] or any(g['status']=='fail' for g in groups) else 'warn' if counts['warn'] or any(g['status']=='warn' for g in groups) else 'ok'
print(json.dumps({'schema':1, 'host':'pegasus', 'uptimeSeconds':system_uptime_seconds(), 'generatedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'overall':overall, 'counts':counts,'groups':groups,'checks':checks, 'diagnostics':plain[-24000:], 'commandExitCode':p.returncode}, ensure_ascii=False))
+20 -1
View File
@@ -62,7 +62,26 @@ for directory in ('/', '/backup'):
systemctl = run(['systemctl', '--failed', '--no-legend', '--plain'])
if systemctl is not None:
failed = [line for line in systemctl.stdout.splitlines() if line.strip()]
check('Services', 'Failed systemd units', 'fail' if failed else 'ok',
# Firmware metadata refresh is noncritical.
# Any other failed systemd unit remains critical.
optional_units = {'fwupd-refresh.service'}
failed_names = [
line.split()[0].lstrip('●')
for line in failed
if line.split()
]
if not failed:
severity = 'ok'
elif failed_names and all(
unit in optional_units for unit in failed_names
):
severity = 'warn'
else:
severity = 'fail'
check('Services', 'Failed systemd units', severity,
', '.join(failed[:5]) if failed else 'None')
if shutil.which('docker'):
+311
View File
@@ -0,0 +1,311 @@
#!/usr/bin/env python
import os
import sys
import time
import socket
import commands
def command(args):
try:
quoted = []
for arg in args:
value = str(arg)
quoted.append("'" + value.replace("'", "'\\''") + "'")
cmd = " ".join(quoted)
return commands.getoutput(cmd).strip()
except:
return ""
def escape(value):
value = str(value)
return value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r")
def string(value):
return '"' + escape(value) + '"'
def status(name, state, detail):
return '{"name":%s,"state":%s,"detail":%s}' % (
string(name), string(state), string(detail)
)
def check(section, name, state, detail):
return '{"section":%s,"name":%s,"status":%s,"detail":%s}' % (
string(section), string(name), string(state), string(detail)
)
def group(name, state):
return '{"name":%s,"status":%s,"missing":[]}' % (
string(name), string(state)
)
def read_file(path):
try:
f = open(path)
data = f.read()
f.close()
return data
except:
return ""
def process_running(name):
output = command(["ps", "ax"])
return name in output
hostname = socket.gethostname()
# CPU
try:
load = float(read_file("/proc/loadavg").split()[0])
except:
load = 0.0
try:
cpu_count = len([
x for x in read_file("/proc/cpuinfo").splitlines()
if x.startswith("processor")
])
if cpu_count < 1:
cpu_count = 1
except:
cpu_count = 1
cpu_state = "ok"
if load > cpu_count * 2:
cpu_state = "fail"
elif load > cpu_count:
cpu_state = "warn"
# Memory
mem = {}
for line in read_file("/proc/meminfo").splitlines():
parts = line.replace(":", "").split()
if len(parts) >= 2:
try:
mem[parts[0]] = int(parts[1])
except:
pass
total = mem.get("MemTotal", 1)
free = (
mem.get("MemFree", 0) +
mem.get("Buffers", 0) +
mem.get("Cached", 0)
)
memory_percent = int(100 * (total - free) / total)
memory_state = "ok"
if memory_percent >= 95:
memory_state = "fail"
elif memory_percent >= 85:
memory_state = "warn"
# Disk
disk_percent = 0
disk_state = "ok"
try:
st = os.statvfs("/")
disk_percent = int(
100 * (st.f_blocks - st.f_bavail) / st.f_blocks
)
except:
pass
if disk_percent >= 95:
disk_state = "fail"
elif disk_percent >= 85:
disk_state = "warn"
# Notes data partition
notes_disk_state = "ok"
notes_disk_percent = 0
notes_disk_detail = "Not checked"
try:
mounts = open("/proc/mounts").read()
mounted = 0
for line in mounts.splitlines():
parts = line.split()
if len(parts) >= 2 and parts[1] == "/local/notesdata":
mounted = 1
break
if not mounted:
notes_disk_state = "fail"
notes_disk_detail = "/local/notesdata is not mounted"
else:
st = os.statvfs("/local/notesdata")
if st.f_blocks > 0:
notes_disk_percent = int(
100 * (st.f_blocks - st.f_bavail) / st.f_blocks
)
notes_disk_detail = "%d%% used" % notes_disk_percent
if notes_disk_percent >= 95:
notes_disk_state = "fail"
elif notes_disk_percent >= 85:
notes_disk_state = "warn"
else:
notes_disk_state = "fail"
notes_disk_detail = "Filesystem size unavailable"
except:
notes_disk_state = "fail"
notes_disk_detail = "Cannot read Notes data filesystem"
# Uptime
try:
uptime = int(float(read_file("/proc/uptime").split()[0]))
except:
uptime = 0
checks = [
check(
"System",
"CPU load",
cpu_state,
"1m load %.2f; %d CPUs" % (load, cpu_count)
),
check(
"System",
"Memory",
memory_state,
"%d%% used" % memory_percent
),
check(
"Storage",
"Disk /",
disk_state,
"%d%% used" % disk_percent
),
check(
"Storage",
"Notes data disk",
notes_disk_state,
notes_disk_detail
)
]
# Legacy SysV services
services = []
service_names = [
("ssh", "sshd"),
("apache2", "apache2"),
("nginx", "nginx"),
("mysql", "mysqld"),
("postgresql", "postgres"),
("redis-server", "redis-server"),
("domino", "server")
]
for service_name, process_name in service_names:
init_script = "/etc/init.d/" + service_name
if not os.path.exists(init_script):
services.append(
status(service_name, "not_installed", "SysV service not found")
)
continue
running = process_running(process_name)
if running:
service_state = "running"
else:
service_state = "stopped"
services.append(
status(
service_name,
service_state,
"SysV process check"
)
)
# Domino is a required service on Europa.
domino_state = "ok"
domino_detail = "Domino process running"
if not process_running("server"):
domino_state = "fail"
domino_detail = "Domino server process not running"
checks.append(
check(
"Services",
"Domino",
domino_state,
domino_detail
)
)
states = [
cpu_state,
memory_state,
disk_state,
notes_disk_state,
domino_state
]
overall = "ok"
if "fail" in states:
overall = "fail"
elif "warn" in states:
overall = "warn"
ok_count = states.count("ok")
warn_count = states.count("warn")
fail_count = states.count("fail")
if cpu_state == "fail" or memory_state == "fail":
system_state = "fail"
elif cpu_state == "warn" or memory_state == "warn":
system_state = "warn"
else:
system_state = "ok"
if disk_state == "fail" or notes_disk_state == "fail":
storage_state = "fail"
elif disk_state == "warn" or notes_disk_state == "warn":
storage_state = "warn"
else:
storage_state = "ok"
groups = [
group("System", system_state),
group("Storage", storage_state),
group("Services", domino_state)
]
output = """{
"schema":1,
"host":%s,
"generatedAt":%s,
"overall":%s,
"counts":{"ok":%d,"warn":%d,"fail":%d},
"groups":[%s],
"checks":[%s],
"diagnostics":null,
"services":[%s],
"uptimeSeconds":%d
}""" % (
string(hostname),
string(time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())),
string(overall),
ok_count,
warn_count,
fail_count,
",".join(groups),
",".join(checks),
",".join(services),
uptime
)
print output