@glassmkr/crucible 0.6.0 → 0.6.1
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.
- package/dist/collect/ntp.js +42 -35
- package/dist/collect/ntp.js.map +1 -1
- package/package.json +9 -2
- package/src/collect/ntp.ts +40 -33
package/dist/collect/ntp.js
CHANGED
|
@@ -2,12 +2,11 @@ import { run } from "../lib/exec.js";
|
|
|
2
2
|
export async function collectNtp() {
|
|
3
3
|
// Try timedatectl first (systemd-timesyncd)
|
|
4
4
|
const tdctl = await run("timedatectl", ["show", "--property=NTPSynchronized", "--value"]);
|
|
5
|
-
if
|
|
5
|
+
// Only trust timedatectl if it returns a clear "yes" or "no"
|
|
6
|
+
if (tdctl !== null && (tdctl.trim() === "yes" || tdctl.trim() === "no")) {
|
|
6
7
|
const synced = tdctl.trim() === "yes";
|
|
7
|
-
// Get the source daemon name
|
|
8
8
|
const statusOut = await run("timedatectl", ["show", "--property=NTP", "--value"]);
|
|
9
9
|
const ntpEnabled = statusOut?.trim() === "yes";
|
|
10
|
-
// Try to get offset from timedatectl timesync-status
|
|
11
10
|
let offset = 0;
|
|
12
11
|
try {
|
|
13
12
|
const tsStatus = await run("timedatectl", ["timesync-status"]);
|
|
@@ -33,47 +32,55 @@ export async function collectNtp() {
|
|
|
33
32
|
daemon_running: ntpEnabled || synced,
|
|
34
33
|
};
|
|
35
34
|
}
|
|
36
|
-
// Try chrony
|
|
35
|
+
// Try chrony - validate output contains expected "Leap status" field
|
|
37
36
|
const chronyOut = await run("chronyc", ["tracking"]);
|
|
38
37
|
if (chronyOut) {
|
|
39
38
|
const leapMatch = chronyOut.match(/Leap status\s*:\s*(.+)/);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
if (leapMatch) {
|
|
40
|
+
// Output looks like real chrony tracking data
|
|
41
|
+
const synced = leapMatch[1].trim() === "Normal";
|
|
42
|
+
let offset = 0;
|
|
43
|
+
const offsetMatch = chronyOut.match(/Last offset\s*:\s*([+-]?\d+\.?\d*)\s*seconds/);
|
|
44
|
+
if (offsetMatch)
|
|
45
|
+
offset = parseFloat(offsetMatch[1]);
|
|
46
|
+
return {
|
|
47
|
+
synced,
|
|
48
|
+
offset_seconds: Math.abs(offset),
|
|
49
|
+
source: "chrony",
|
|
50
|
+
daemon_running: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// chronyc returned output but not tracking data (error message, daemon not running)
|
|
54
|
+
// Fall through to ntpq
|
|
51
55
|
}
|
|
52
|
-
// Try ntpq
|
|
56
|
+
// Try ntpq - validate output contains the header line with "remote"
|
|
53
57
|
const ntpqOut = await run("ntpq", ["-pn"]);
|
|
54
58
|
if (ntpqOut) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
const hasHeader = ntpqOut.split("\n").some((line) => line.includes("remote"));
|
|
60
|
+
if (hasHeader) {
|
|
61
|
+
// Output looks like real ntpq peer table
|
|
62
|
+
const synced = ntpqOut.split("\n").some((line) => line.startsWith("*"));
|
|
63
|
+
let offset = 0;
|
|
64
|
+
const selectedLine = ntpqOut.split("\n").find((line) => line.startsWith("*"));
|
|
65
|
+
if (selectedLine) {
|
|
66
|
+
const fields = selectedLine.trim().split(/\s+/);
|
|
67
|
+
if (fields.length >= 9) {
|
|
68
|
+
const rawOffset = parseFloat(fields[8]);
|
|
69
|
+
if (!isNaN(rawOffset))
|
|
70
|
+
offset = Math.abs(rawOffset) / 1000;
|
|
71
|
+
}
|
|
67
72
|
}
|
|
73
|
+
return {
|
|
74
|
+
synced,
|
|
75
|
+
offset_seconds: offset,
|
|
76
|
+
source: "ntpd",
|
|
77
|
+
daemon_running: true,
|
|
78
|
+
};
|
|
68
79
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
offset_seconds: offset,
|
|
72
|
-
source: "ntpd",
|
|
73
|
-
daemon_running: true,
|
|
74
|
-
};
|
|
80
|
+
// ntpq returned output but not peer table (error message, daemon not running)
|
|
81
|
+
// Fall through to "none"
|
|
75
82
|
}
|
|
76
|
-
// No time sync daemon found
|
|
83
|
+
// No time sync daemon found (or all probes returned invalid output)
|
|
77
84
|
return {
|
|
78
85
|
synced: false,
|
|
79
86
|
offset_seconds: 0,
|
package/dist/collect/ntp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ntp.js","sourceRoot":"","sources":["../../src/collect/ntp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AASrC,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,4CAA4C;IAC5C,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,4BAA4B,EAAE,SAAS,CAAC,CAAC,CAAC;IAC1F,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"ntp.js","sourceRoot":"","sources":["../../src/collect/ntp.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassmkr/crucible",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Lightweight bare metal server monitoring. IPMI, SMART, OS, network. Opinionated alerts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
13
|
"clean": "rm -rf dist"
|
|
14
14
|
},
|
|
15
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"monitoring",
|
|
17
|
+
"bare-metal",
|
|
18
|
+
"ipmi",
|
|
19
|
+
"smart",
|
|
20
|
+
"server",
|
|
21
|
+
"alerts"
|
|
22
|
+
],
|
|
16
23
|
"license": "MIT",
|
|
17
24
|
"repository": {
|
|
18
25
|
"type": "git",
|
package/src/collect/ntp.ts
CHANGED
|
@@ -10,13 +10,12 @@ export interface NtpData {
|
|
|
10
10
|
export async function collectNtp(): Promise<NtpData> {
|
|
11
11
|
// Try timedatectl first (systemd-timesyncd)
|
|
12
12
|
const tdctl = await run("timedatectl", ["show", "--property=NTPSynchronized", "--value"]);
|
|
13
|
-
if
|
|
13
|
+
// Only trust timedatectl if it returns a clear "yes" or "no"
|
|
14
|
+
if (tdctl !== null && (tdctl.trim() === "yes" || tdctl.trim() === "no")) {
|
|
14
15
|
const synced = tdctl.trim() === "yes";
|
|
15
|
-
// Get the source daemon name
|
|
16
16
|
const statusOut = await run("timedatectl", ["show", "--property=NTP", "--value"]);
|
|
17
17
|
const ntpEnabled = statusOut?.trim() === "yes";
|
|
18
18
|
|
|
19
|
-
// Try to get offset from timedatectl timesync-status
|
|
20
19
|
let offset = 0;
|
|
21
20
|
try {
|
|
22
21
|
const tsStatus = await run("timedatectl", ["timesync-status"]);
|
|
@@ -40,47 +39,55 @@ export async function collectNtp(): Promise<NtpData> {
|
|
|
40
39
|
};
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
// Try chrony
|
|
42
|
+
// Try chrony - validate output contains expected "Leap status" field
|
|
44
43
|
const chronyOut = await run("chronyc", ["tracking"]);
|
|
45
44
|
if (chronyOut) {
|
|
46
45
|
const leapMatch = chronyOut.match(/Leap status\s*:\s*(.+)/);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
if (leapMatch) {
|
|
47
|
+
// Output looks like real chrony tracking data
|
|
48
|
+
const synced = leapMatch[1].trim() === "Normal";
|
|
49
|
+
let offset = 0;
|
|
50
|
+
const offsetMatch = chronyOut.match(/Last offset\s*:\s*([+-]?\d+\.?\d*)\s*seconds/);
|
|
51
|
+
if (offsetMatch) offset = parseFloat(offsetMatch[1]);
|
|
52
|
+
return {
|
|
53
|
+
synced,
|
|
54
|
+
offset_seconds: Math.abs(offset),
|
|
55
|
+
source: "chrony",
|
|
56
|
+
daemon_running: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// chronyc returned output but not tracking data (error message, daemon not running)
|
|
60
|
+
// Fall through to ntpq
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
// Try ntpq
|
|
63
|
+
// Try ntpq - validate output contains the header line with "remote"
|
|
60
64
|
const ntpqOut = await run("ntpq", ["-pn"]);
|
|
61
65
|
if (ntpqOut) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
const hasHeader = ntpqOut.split("\n").some((line) => line.includes("remote"));
|
|
67
|
+
if (hasHeader) {
|
|
68
|
+
// Output looks like real ntpq peer table
|
|
69
|
+
const synced = ntpqOut.split("\n").some((line) => line.startsWith("*"));
|
|
70
|
+
let offset = 0;
|
|
71
|
+
const selectedLine = ntpqOut.split("\n").find((line) => line.startsWith("*"));
|
|
72
|
+
if (selectedLine) {
|
|
73
|
+
const fields = selectedLine.trim().split(/\s+/);
|
|
74
|
+
if (fields.length >= 9) {
|
|
75
|
+
const rawOffset = parseFloat(fields[8]);
|
|
76
|
+
if (!isNaN(rawOffset)) offset = Math.abs(rawOffset) / 1000;
|
|
77
|
+
}
|
|
73
78
|
}
|
|
79
|
+
return {
|
|
80
|
+
synced,
|
|
81
|
+
offset_seconds: offset,
|
|
82
|
+
source: "ntpd",
|
|
83
|
+
daemon_running: true,
|
|
84
|
+
};
|
|
74
85
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
offset_seconds: offset,
|
|
78
|
-
source: "ntpd",
|
|
79
|
-
daemon_running: true,
|
|
80
|
-
};
|
|
86
|
+
// ntpq returned output but not peer table (error message, daemon not running)
|
|
87
|
+
// Fall through to "none"
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
// No time sync daemon found
|
|
90
|
+
// No time sync daemon found (or all probes returned invalid output)
|
|
84
91
|
return {
|
|
85
92
|
synced: false,
|
|
86
93
|
offset_seconds: 0,
|