@glassmkr/crucible 0.6.6 → 0.7.0
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/README.md +25 -0
- package/dist/__tests__/reboot-marker.test.d.ts +1 -0
- package/dist/__tests__/reboot-marker.test.js +110 -0
- package/dist/__tests__/reboot-marker.test.js.map +1 -0
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -1
- package/dist/collect/__tests__/system.test.d.ts +1 -0
- package/dist/collect/__tests__/system.test.js +25 -0
- package/dist/collect/__tests__/system.test.js.map +1 -0
- package/dist/collect/system.d.ts +1 -0
- package/dist/collect/system.js +10 -0
- package/dist/collect/system.js.map +1 -1
- package/dist/index.js +54 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/reboot-marker.d.ts +35 -0
- package/dist/lib/reboot-marker.js +95 -0
- package/dist/lib/reboot-marker.js.map +1 -0
- package/dist/lib/types.d.ts +8 -0
- package/package.json +1 -1
- package/src/__tests__/reboot-marker.test.ts +122 -0
- package/src/cli.ts +51 -1
- package/src/collect/__tests__/system.test.ts +29 -0
- package/src/collect/system.ts +11 -0
- package/src/index.ts +53 -4
- package/src/lib/reboot-marker.ts +88 -0
- package/src/lib/types.ts +12 -0
package/README.md
CHANGED
|
@@ -92,6 +92,8 @@ Images are published to [ghcr.io/glassmkr/crucible](https://github.com/glassmkr/
|
|
|
92
92
|
|
|
93
93
|
```
|
|
94
94
|
glassmkr-crucible [options]
|
|
95
|
+
glassmkr-crucible mark-reboot [--reason TEXT] [--ttl DURATION]
|
|
96
|
+
glassmkr-crucible reboot [--reason TEXT] [--ttl DURATION]
|
|
95
97
|
|
|
96
98
|
Options:
|
|
97
99
|
-v, --version Print version and exit
|
|
@@ -101,6 +103,29 @@ Options:
|
|
|
101
103
|
|
|
102
104
|
`--config=PATH` and the legacy positional form `glassmkr-crucible /path/to.yaml` both work. Without options, Crucible runs as a long-lived collector daemon.
|
|
103
105
|
|
|
106
|
+
## Rebooting without noise
|
|
107
|
+
|
|
108
|
+
Crucible distinguishes planned reboots from unplanned ones and gives each rule a short grace period after boot so that transient conditions (bond slave still negotiating, clock not synced yet) do not page you.
|
|
109
|
+
|
|
110
|
+
Before a planned reboot:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
sudo glassmkr-crucible reboot --reason "kernel update"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Or, if you prefer to trigger the reboot yourself:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
sudo glassmkr-crucible mark-reboot --reason "kernel update"
|
|
120
|
+
sudo reboot
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Both write a short-lived marker to `/var/lib/crucible/reboot-expected`. The agent reads it once on startup, sets `expected_reboot: true` on the first post-boot snapshot, and deletes the file. Forge reads that flag and suppresses the `server_rebooted_unexpectedly` alert for that boot only.
|
|
124
|
+
|
|
125
|
+
The marker is single-use and expires 10 minutes after it is written (override with `--ttl 5m` / `--ttl 1h`), so a forgotten marker cannot silence a genuine crash reboot next week. If systemd fails to reboot the host, the marker simply expires on its own.
|
|
126
|
+
|
|
127
|
+
Per-rule grace windows are applied separately: bond-slave-down and CPU-temperature get 60 s, interface errors 120 s, clock-sync / NTP 300 s, others 0 s. Suppressed evaluations are recorded in alert history with status `suppressed_boot_grace` or `suppressed_planned_reboot` so you can audit exactly why a rule didn't fire during a given boot.
|
|
128
|
+
|
|
104
129
|
## Systemd Service
|
|
105
130
|
|
|
106
131
|
Create `/etc/systemd/system/glassmkr-crucible.service`:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, existsSync, writeFileSync, statSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { consumeRebootMarker, writeRebootMarker, parseDuration, } from "../lib/reboot-marker.js";
|
|
6
|
+
import { parseCliArgs } from "../cli.js";
|
|
7
|
+
let tmpDir;
|
|
8
|
+
let path;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = mkdtempSync(join(tmpdir(), "crucible-test-"));
|
|
11
|
+
path = join(tmpDir, "reboot-expected");
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
try {
|
|
15
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
});
|
|
19
|
+
describe("consumeRebootMarker", () => {
|
|
20
|
+
it("7. marker present, not expired: returns flag, deletes file", () => {
|
|
21
|
+
const future = new Date(Date.now() + 5 * 60_000).toISOString();
|
|
22
|
+
writeFileSync(path, JSON.stringify({ expires_at: future, reason: "kernel update" }));
|
|
23
|
+
const out = consumeRebootMarker(path);
|
|
24
|
+
expect(out).toEqual({ expected: true, reason: "kernel update" });
|
|
25
|
+
expect(existsSync(path)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it("8. marker present, expired: returns null, deletes file", () => {
|
|
28
|
+
const past = new Date(Date.now() - 60_000).toISOString();
|
|
29
|
+
writeFileSync(path, JSON.stringify({ expires_at: past, reason: "stale" }));
|
|
30
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
31
|
+
expect(existsSync(path)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
it("9. marker absent: returns null, no throw", () => {
|
|
34
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
it("15. malformed JSON: returns null, file deleted, no crash", () => {
|
|
37
|
+
writeFileSync(path, "{not json at all");
|
|
38
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
39
|
+
expect(existsSync(path)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it("invalid expires_at (missing): returns null, file deleted", () => {
|
|
42
|
+
writeFileSync(path, JSON.stringify({ reason: "oops" }));
|
|
43
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
44
|
+
expect(existsSync(path)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("consumed marker cannot be re-read (single-use)", () => {
|
|
47
|
+
const future = new Date(Date.now() + 60_000).toISOString();
|
|
48
|
+
writeFileSync(path, JSON.stringify({ expires_at: future }));
|
|
49
|
+
expect(consumeRebootMarker(path)).not.toBeNull();
|
|
50
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("writeRebootMarker", () => {
|
|
54
|
+
it("13. writes file at given path with correct TTL and reason, 0600 mode", () => {
|
|
55
|
+
const now = new Date("2026-04-21T22:00:00Z");
|
|
56
|
+
const res = writeRebootMarker({ path, reason: "kernel update", ttlMs: 10 * 60_000, now });
|
|
57
|
+
expect(res.path).toBe(path);
|
|
58
|
+
expect(res.expires_at).toBe("2026-04-21T22:10:00.000Z");
|
|
59
|
+
expect(existsSync(path)).toBe(true);
|
|
60
|
+
const mode = statSync(path).mode & 0o777;
|
|
61
|
+
expect(mode).toBe(0o600);
|
|
62
|
+
const round = consumeRebootMarker(path, new Date("2026-04-21T22:05:00Z"));
|
|
63
|
+
expect(round).toEqual({ expected: true, reason: "kernel update" });
|
|
64
|
+
});
|
|
65
|
+
it("default TTL is 10 minutes", () => {
|
|
66
|
+
const now = new Date("2026-04-21T22:00:00Z");
|
|
67
|
+
const res = writeRebootMarker({ path, now });
|
|
68
|
+
expect(res.expires_at).toBe("2026-04-21T22:10:00.000Z");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("parseDuration", () => {
|
|
72
|
+
it.each([
|
|
73
|
+
["10m", 600_000],
|
|
74
|
+
["2h", 7_200_000],
|
|
75
|
+
["600s", 600_000],
|
|
76
|
+
["500ms", 500],
|
|
77
|
+
["30", 30_000], // bare number -> seconds
|
|
78
|
+
])("%s -> %d ms", (input, ms) => {
|
|
79
|
+
expect(parseDuration(input)).toBe(ms);
|
|
80
|
+
});
|
|
81
|
+
it("rejects garbage", () => {
|
|
82
|
+
expect(parseDuration("forever")).toBeNull();
|
|
83
|
+
expect(parseDuration("-5m")).toBeNull();
|
|
84
|
+
expect(parseDuration("")).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("CLI parseCliArgs subcommands", () => {
|
|
88
|
+
it("14. `reboot` subcommand captured with flags", () => {
|
|
89
|
+
const { result } = parseCliArgs(["reboot", "--reason", "kernel update"], "1.0.0");
|
|
90
|
+
expect(result.mode).toBe("reboot");
|
|
91
|
+
expect(result.reason).toBe("kernel update");
|
|
92
|
+
});
|
|
93
|
+
it("`mark-reboot` with --ttl parsed through", () => {
|
|
94
|
+
const { result } = parseCliArgs(["mark-reboot", "--ttl=5m", "--reason=test"], "1.0.0");
|
|
95
|
+
expect(result.mode).toBe("mark-reboot");
|
|
96
|
+
expect(result.ttl).toBe("5m");
|
|
97
|
+
expect(result.reason).toBe("test");
|
|
98
|
+
});
|
|
99
|
+
it("`mark-reboot --help` returns help output without running", () => {
|
|
100
|
+
const { result, output } = parseCliArgs(["mark-reboot", "--help"], "1.0.0");
|
|
101
|
+
expect(result.mode).toBe("help");
|
|
102
|
+
expect(output).toContain("mark-reboot");
|
|
103
|
+
});
|
|
104
|
+
it("top-level help lists the new subcommands", () => {
|
|
105
|
+
const { output } = parseCliArgs(["--help"], "1.0.0");
|
|
106
|
+
expect(output).toMatch(/mark-reboot/);
|
|
107
|
+
expect(output).toMatch(/reboot/);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=reboot-marker.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reboot-marker.test.js","sourceRoot":"","sources":["../../src/__tests__/reboot-marker.test.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
export type CliMode = "version" | "help" | "run" | "mark-reboot" | "reboot";
|
|
1
2
|
export interface CliArgs {
|
|
2
|
-
mode:
|
|
3
|
+
mode: CliMode;
|
|
3
4
|
configPath: string;
|
|
5
|
+
reason?: string;
|
|
6
|
+
ttl?: string;
|
|
4
7
|
}
|
|
5
8
|
export declare const DEFAULT_CONFIG_PATH = "/etc/glassmkr/collector.yaml";
|
|
6
9
|
export declare function parseCliArgs(argv: string[], version: string): {
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,36 @@ export const DEFAULT_CONFIG_PATH = "/etc/glassmkr/collector.yaml";
|
|
|
5
5
|
export function parseCliArgs(argv, version) {
|
|
6
6
|
// argv is typically process.argv.slice(2)
|
|
7
7
|
let configPath = DEFAULT_CONFIG_PATH;
|
|
8
|
+
// Subcommand dispatch: `mark-reboot` and `reboot` take their own flags
|
|
9
|
+
// (--reason, --ttl) but re-use --help.
|
|
10
|
+
if (argv[0] === "mark-reboot" || argv[0] === "reboot") {
|
|
11
|
+
const mode = argv[0];
|
|
12
|
+
let reason;
|
|
13
|
+
let ttl;
|
|
14
|
+
for (let i = 1; i < argv.length; i++) {
|
|
15
|
+
const a = argv[i];
|
|
16
|
+
if (a === "--help" || a === "-h") {
|
|
17
|
+
return { result: { mode: "help", configPath: "" }, output: subcommandHelp(mode, version) };
|
|
18
|
+
}
|
|
19
|
+
if (a === "--reason") {
|
|
20
|
+
reason = argv[++i];
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (a.startsWith("--reason=")) {
|
|
24
|
+
reason = a.slice("--reason=".length);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (a === "--ttl") {
|
|
28
|
+
ttl = argv[++i];
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (a.startsWith("--ttl=")) {
|
|
32
|
+
ttl = a.slice("--ttl=".length);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { result: { mode, configPath: "", reason, ttl }, output: null };
|
|
37
|
+
}
|
|
8
38
|
for (let i = 0; i < argv.length; i++) {
|
|
9
39
|
const arg = argv[i];
|
|
10
40
|
if (arg === "--version" || arg === "-v") {
|
|
@@ -40,14 +70,40 @@ export function helpText(version) {
|
|
|
40
70
|
"",
|
|
41
71
|
"Usage:",
|
|
42
72
|
" glassmkr-crucible [options]",
|
|
73
|
+
" glassmkr-crucible mark-reboot [--reason TEXT] [--ttl DURATION]",
|
|
74
|
+
" glassmkr-crucible reboot [--reason TEXT] [--ttl DURATION]",
|
|
43
75
|
"",
|
|
44
76
|
"Options:",
|
|
45
77
|
" -v, --version Print version and exit",
|
|
46
78
|
" -h, --help Print this help and exit",
|
|
47
79
|
` -c, --config Path to config file (default: ${DEFAULT_CONFIG_PATH})`,
|
|
48
80
|
"",
|
|
81
|
+
"Subcommands:",
|
|
82
|
+
" mark-reboot Write a planned-reboot marker so the next boot",
|
|
83
|
+
" does not fire `server_rebooted_unexpectedly`.",
|
|
84
|
+
" You run the reboot yourself afterwards.",
|
|
85
|
+
" reboot Write the marker, then invoke `systemctl reboot`.",
|
|
86
|
+
"",
|
|
49
87
|
"Without options, starts the collector daemon using the config file.",
|
|
50
88
|
"Docs: https://github.com/glassmkr/crucible",
|
|
51
89
|
].join("\n");
|
|
52
90
|
}
|
|
91
|
+
function subcommandHelp(mode, version) {
|
|
92
|
+
const action = mode === "reboot"
|
|
93
|
+
? "Write a planned-reboot marker and invoke `systemctl reboot`."
|
|
94
|
+
: "Write a planned-reboot marker; operator triggers the reboot.";
|
|
95
|
+
return [
|
|
96
|
+
`glassmkr-crucible ${mode} - ${action}`,
|
|
97
|
+
"",
|
|
98
|
+
"Usage:",
|
|
99
|
+
` glassmkr-crucible ${mode} [--reason TEXT] [--ttl DURATION]`,
|
|
100
|
+
"",
|
|
101
|
+
"Options:",
|
|
102
|
+
' --reason TEXT Free-text reason (e.g. "kernel update")',
|
|
103
|
+
" --ttl DURATION Expiry window; e.g. 5m, 10m, 1h (default 10m)",
|
|
104
|
+
"",
|
|
105
|
+
`Marker path: /var/lib/crucible/reboot-expected (requires root).`,
|
|
106
|
+
`v${version}`,
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
53
109
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,6EAA6E;AAC7E,8EAA8E;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readOsReleaseField } from "../system.js";
|
|
3
|
+
describe("readOsReleaseField", () => {
|
|
4
|
+
it("parses unquoted Ubuntu values", () => {
|
|
5
|
+
const s = 'NAME="Ubuntu"\nID=ubuntu\nID_LIKE=debian\nVERSION_ID="24.04"';
|
|
6
|
+
expect(readOsReleaseField(s, "ID")).toBe("ubuntu");
|
|
7
|
+
expect(readOsReleaseField(s, "ID_LIKE")).toBe("debian");
|
|
8
|
+
});
|
|
9
|
+
it("parses quoted RHEL-family values", () => {
|
|
10
|
+
const s = 'NAME="Rocky Linux"\nID="rocky"\nID_LIKE="rhel centos fedora"';
|
|
11
|
+
expect(readOsReleaseField(s, "ID")).toBe("rocky");
|
|
12
|
+
expect(readOsReleaseField(s, "ID_LIKE")).toBe("rhel centos fedora");
|
|
13
|
+
});
|
|
14
|
+
it("lowercases the result (some distros uppercase their ID)", () => {
|
|
15
|
+
expect(readOsReleaseField("ID=Alpine", "ID")).toBe("alpine");
|
|
16
|
+
});
|
|
17
|
+
it("returns undefined for a missing key", () => {
|
|
18
|
+
expect(readOsReleaseField("ID=arch", "ID_LIKE")).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
it("does not confuse ID with VERSION_ID", () => {
|
|
21
|
+
const s = 'VERSION_ID="24.04"\nID=ubuntu';
|
|
22
|
+
expect(readOsReleaseField(s, "ID")).toBe("ubuntu");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=system.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/system.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,8DAA8D,CAAC;QACzE,MAAM,CAAC,kBAAkB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnD,MAAM,CAAC,kBAAkB,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,GAAG,8DAA8D,CAAC;QACzE,MAAM,CAAC,kBAAkB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAClD,MAAM,CAAC,kBAAkB,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,kBAAkB,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,kBAAkB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,GAAG,+BAA+B,CAAC;QAC1C,MAAM,CAAC,kBAAkB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/collect/system.d.ts
CHANGED
package/dist/collect/system.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { hostname } from "os";
|
|
2
2
|
import { readProcFile } from "../lib/parse.js";
|
|
3
3
|
import { run } from "../lib/exec.js";
|
|
4
|
+
// Matches KEY=value with optional surrounding double quotes. Handles both
|
|
5
|
+
// `ID=ubuntu` and `ID="rocky"` styles found in the wild.
|
|
6
|
+
export function readOsReleaseField(osRelease, key) {
|
|
7
|
+
const m = osRelease.match(new RegExp(`^${key}=("?)(.+?)\\1$`, "m"));
|
|
8
|
+
return m ? m[2].toLowerCase() : undefined;
|
|
9
|
+
}
|
|
4
10
|
export async function collectSystem() {
|
|
5
11
|
const osRelease = readProcFile("/etc/os-release") || "";
|
|
6
12
|
const osName = osRelease.match(/PRETTY_NAME="(.+?)"/)?.[1] || "Unknown";
|
|
13
|
+
const os_id = readOsReleaseField(osRelease, "ID");
|
|
14
|
+
const os_id_like = readOsReleaseField(osRelease, "ID_LIKE");
|
|
7
15
|
const kernel = (await run("uname", ["-r"]))?.trim() || "unknown";
|
|
8
16
|
const uptimeRaw = readProcFile("/proc/uptime") || "0";
|
|
9
17
|
const uptimeSeconds = Math.floor(parseFloat(uptimeRaw.split(" ")[0]));
|
|
@@ -12,6 +20,8 @@ export async function collectSystem() {
|
|
|
12
20
|
hostname: hostname(),
|
|
13
21
|
ip,
|
|
14
22
|
os: osName,
|
|
23
|
+
...(os_id ? { os_id } : {}),
|
|
24
|
+
...(os_id_like ? { os_id_like } : {}),
|
|
15
25
|
kernel,
|
|
16
26
|
uptime_seconds: uptimeSeconds,
|
|
17
27
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system.js","sourceRoot":"","sources":["../../src/collect/system.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAGrC,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,SAAS,GAAG,YAAY,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;IACxD,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;IACxE,MAAM,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IACjE,MAAM,SAAS,GAAG,YAAY,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC;IACtD,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtE,MAAM,EAAE,GAAG,CAAC,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;IAE9E,OAAO;QACL,QAAQ,EAAE,QAAQ,EAAE;QACpB,EAAE;QACF,EAAE,EAAE,MAAM;QACV,MAAM;QACN,cAAc,EAAE,aAAa;KAC9B,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"system.js","sourceRoot":"","sources":["../../src/collect/system.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAGrC,0EAA0E;AAC1E,yDAAyD;AACzD,MAAM,UAAU,kBAAkB,CAAC,SAAiB,EAAE,GAAW;IAC/D,MAAM,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,GAAG,gBAAgB,EAAE,GAAG,CAAC,CAAC,CAAC;IACpE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,SAAS,GAAG,YAAY,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;IACxD,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;IACxE,MAAM,KAAK,GAAG,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAClD,MAAM,UAAU,GAAG,kBAAkB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IACjE,MAAM,SAAS,GAAG,YAAY,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC;IACtD,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtE,MAAM,EAAE,GAAG,CAAC,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;IAE9E,OAAO;QACL,QAAQ,EAAE,QAAQ,EAAE;QACpB,EAAE;QACF,EAAE,EAAE,MAAM;QACV,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3B,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrC,MAAM;QACN,cAAc,EAAE,aAAa;KAC9B,CAAC;AACJ,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -13,14 +13,46 @@ const PKG_VERSION = (() => {
|
|
|
13
13
|
return "0.0.0";
|
|
14
14
|
}
|
|
15
15
|
})();
|
|
16
|
-
// Handle --version
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// Handle --version, --help, and planned-reboot subcommands before
|
|
17
|
+
// importing collectors, loading config, or starting the Prometheus
|
|
18
|
+
// server. Keeps the CLI responsive even on hosts missing the config
|
|
19
|
+
// file or external tools.
|
|
19
20
|
const { result: cliArgs, output: cliOutput } = parseCliArgs(process.argv.slice(2), PKG_VERSION);
|
|
20
|
-
if (cliArgs.mode
|
|
21
|
+
if (cliArgs.mode === "version" || cliArgs.mode === "help") {
|
|
21
22
|
console.log(cliOutput);
|
|
22
23
|
process.exit(0);
|
|
23
24
|
}
|
|
25
|
+
if (cliArgs.mode === "mark-reboot" || cliArgs.mode === "reboot") {
|
|
26
|
+
const { writeRebootMarker, parseDuration, DEFAULT_TTL_MS } = await import("./lib/reboot-marker.js");
|
|
27
|
+
const ttlMs = cliArgs.ttl ? parseDuration(cliArgs.ttl) : DEFAULT_TTL_MS;
|
|
28
|
+
if (ttlMs === null) {
|
|
29
|
+
console.error(`[mark-reboot] invalid --ttl value: ${cliArgs.ttl}. Use e.g. 10m, 2h, 600s.`);
|
|
30
|
+
process.exit(2);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const { path, expires_at } = writeRebootMarker({
|
|
34
|
+
reason: cliArgs.reason, ttlMs,
|
|
35
|
+
});
|
|
36
|
+
console.log(`[${cliArgs.mode}] marker written: ${path} (expires ${expires_at}${cliArgs.reason ? `, reason: ${cliArgs.reason}` : ""})`);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(`[${cliArgs.mode}] failed to write marker: ${err?.message || err}`);
|
|
40
|
+
console.error(` Most likely cause: need root privileges to write under /var/lib/crucible/.`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (cliArgs.mode === "reboot") {
|
|
44
|
+
const { execFileSync } = await import("node:child_process");
|
|
45
|
+
console.log("[reboot] invoking systemctl reboot");
|
|
46
|
+
try {
|
|
47
|
+
execFileSync("systemctl", ["reboot"], { stdio: "inherit" });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error(`[reboot] systemctl reboot failed: ${err?.message || err}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
24
56
|
import { loadConfig } from "./config.js";
|
|
25
57
|
import { checkForUpdates } from "./lib/version-check.js";
|
|
26
58
|
import { startMetricsServer, updateMetrics } from "./metrics-server.js";
|
|
@@ -47,6 +79,16 @@ import { collectConntrack } from "./collect/conntrack.js";
|
|
|
47
79
|
import { collectSystemd } from "./collect/systemd.js";
|
|
48
80
|
import { collectNtp } from "./collect/ntp.js";
|
|
49
81
|
import { collectFileDescriptors } from "./collect/fd.js";
|
|
82
|
+
import { consumeRebootMarker } from "./lib/reboot-marker.js";
|
|
83
|
+
// Consume the planned-reboot marker once at startup. If the operator ran
|
|
84
|
+
// `crucible-agent mark-reboot` / `reboot` before this boot, the marker
|
|
85
|
+
// exists, we flag it on the first snapshot, and we delete the file (so
|
|
86
|
+
// subsequent snapshots don't keep claiming the reboot was planned).
|
|
87
|
+
const plannedRebootFlag = consumeRebootMarker();
|
|
88
|
+
if (plannedRebootFlag) {
|
|
89
|
+
console.log(`[collector] Planned reboot acknowledged${plannedRebootFlag.reason ? `: ${plannedRebootFlag.reason}` : ""}`);
|
|
90
|
+
}
|
|
91
|
+
let plannedRebootConsumed = false;
|
|
50
92
|
const config = loadConfig(cliArgs.configPath);
|
|
51
93
|
console.log(`[collector] Starting. Server: ${config.server_name}. Interval: ${config.collection.interval_seconds}s`);
|
|
52
94
|
console.log(`[collector] IPMI: ${config.collection.ipmi ? "enabled" : "disabled"}, SMART: ${config.collection.smart ? "enabled" : "disabled"}`);
|
|
@@ -96,6 +138,14 @@ async function collect() {
|
|
|
96
138
|
system, cpu, memory, disks, smart, network, raid, ipmi, os_alerts: osAlerts,
|
|
97
139
|
security: cachedSecurity,
|
|
98
140
|
};
|
|
141
|
+
// Single-shot: the very first snapshot after a marked reboot carries
|
|
142
|
+
// the flag, subsequent snapshots do not.
|
|
143
|
+
if (plannedRebootFlag && !plannedRebootConsumed) {
|
|
144
|
+
snapshot.expected_reboot = true;
|
|
145
|
+
if (plannedRebootFlag.reason)
|
|
146
|
+
snapshot.expected_reboot_reason = plannedRebootFlag.reason;
|
|
147
|
+
plannedRebootConsumed = true;
|
|
148
|
+
}
|
|
99
149
|
// ZFS and I/O errors: collect every cycle (lightweight checks)
|
|
100
150
|
try {
|
|
101
151
|
snapshot.zfs = await collectZfs() ?? undefined;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const DEFAULT_MARKER_PATH = "/var/lib/crucible/reboot-expected";
|
|
2
|
+
export declare const DEFAULT_TTL_MS: number;
|
|
3
|
+
export interface PlannedReboot {
|
|
4
|
+
expected: true;
|
|
5
|
+
reason?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface RebootMarker {
|
|
8
|
+
expires_at: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read and delete the marker at `path`. Returns the resolved reboot flag
|
|
13
|
+
* if the file existed, was parseable JSON, and hasn't expired; otherwise
|
|
14
|
+
* returns null. The file is unlinked in every branch where it existed,
|
|
15
|
+
* so a malformed or stale marker is one-shot (can't linger).
|
|
16
|
+
*/
|
|
17
|
+
export declare function consumeRebootMarker(path?: string, now?: Date): PlannedReboot | null;
|
|
18
|
+
/**
|
|
19
|
+
* Write a planned-reboot marker. Used by the `mark-reboot` and `reboot`
|
|
20
|
+
* CLI subcommands. `ttlMs` defaults to 10 minutes. Creates the parent
|
|
21
|
+
* directory if needed. Chmod 600 so other users on the host can't read
|
|
22
|
+
* or modify it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function writeRebootMarker(opts: {
|
|
25
|
+
reason?: string;
|
|
26
|
+
ttlMs?: number;
|
|
27
|
+
path?: string;
|
|
28
|
+
now?: Date;
|
|
29
|
+
}): {
|
|
30
|
+
path: string;
|
|
31
|
+
expires_at: string;
|
|
32
|
+
};
|
|
33
|
+
/** Parse a duration like "10m", "2h", "600s" into milliseconds. Used by
|
|
34
|
+
* the CLI for the `--ttl` flag. */
|
|
35
|
+
export declare function parseDuration(s: string): number | null;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Planned-reboot marker handling.
|
|
2
|
+
//
|
|
3
|
+
// An operator signals "the next reboot is expected, don't page me"
|
|
4
|
+
// by writing a short-lived JSON file to disk BEFORE rebooting. The
|
|
5
|
+
// collector reads and deletes it on agent startup; the first
|
|
6
|
+
// post-boot snapshot then carries `expected_reboot: true` so Forge's
|
|
7
|
+
// unexpected_reboot rule stays quiet.
|
|
8
|
+
//
|
|
9
|
+
// Single-use (deleted on read regardless of validity) and TTL-guarded
|
|
10
|
+
// (default 10 min) so a forgotten marker cannot silence a genuine
|
|
11
|
+
// crash reboot weeks later.
|
|
12
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
13
|
+
import { dirname } from "node:path";
|
|
14
|
+
export const DEFAULT_MARKER_PATH = "/var/lib/crucible/reboot-expected";
|
|
15
|
+
export const DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
16
|
+
/**
|
|
17
|
+
* Read and delete the marker at `path`. Returns the resolved reboot flag
|
|
18
|
+
* if the file existed, was parseable JSON, and hasn't expired; otherwise
|
|
19
|
+
* returns null. The file is unlinked in every branch where it existed,
|
|
20
|
+
* so a malformed or stale marker is one-shot (can't linger).
|
|
21
|
+
*/
|
|
22
|
+
export function consumeRebootMarker(path = DEFAULT_MARKER_PATH, now = new Date()) {
|
|
23
|
+
if (!existsSync(path))
|
|
24
|
+
return null;
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = readFileSync(path, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
try {
|
|
31
|
+
unlinkSync(path);
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// Always delete after read, regardless of validity.
|
|
37
|
+
try {
|
|
38
|
+
unlinkSync(path);
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(raw);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.expires_at !== "string")
|
|
49
|
+
return null;
|
|
50
|
+
const expiresAt = new Date(parsed.expires_at);
|
|
51
|
+
if (isNaN(expiresAt.getTime()))
|
|
52
|
+
return null;
|
|
53
|
+
if (expiresAt.getTime() <= now.getTime())
|
|
54
|
+
return null; // stale
|
|
55
|
+
return { expected: true, reason: parsed.reason };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Write a planned-reboot marker. Used by the `mark-reboot` and `reboot`
|
|
59
|
+
* CLI subcommands. `ttlMs` defaults to 10 minutes. Creates the parent
|
|
60
|
+
* directory if needed. Chmod 600 so other users on the host can't read
|
|
61
|
+
* or modify it.
|
|
62
|
+
*/
|
|
63
|
+
export function writeRebootMarker(opts) {
|
|
64
|
+
const path = opts.path ?? DEFAULT_MARKER_PATH;
|
|
65
|
+
const now = opts.now ?? new Date();
|
|
66
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
67
|
+
const expiresAt = new Date(now.getTime() + ttlMs);
|
|
68
|
+
const body = { expires_at: expiresAt.toISOString() };
|
|
69
|
+
if (opts.reason)
|
|
70
|
+
body.reason = opts.reason;
|
|
71
|
+
try {
|
|
72
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
writeFileSync(path, JSON.stringify(body), { mode: 0o600 });
|
|
76
|
+
try {
|
|
77
|
+
chmodSync(path, 0o600);
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
return { path, expires_at: body.expires_at };
|
|
81
|
+
}
|
|
82
|
+
/** Parse a duration like "10m", "2h", "600s" into milliseconds. Used by
|
|
83
|
+
* the CLI for the `--ttl` flag. */
|
|
84
|
+
export function parseDuration(s) {
|
|
85
|
+
const m = /^(\d+)\s*(ms|s|m|h)?$/.exec(s.trim());
|
|
86
|
+
if (!m)
|
|
87
|
+
return null;
|
|
88
|
+
const n = parseInt(m[1], 10);
|
|
89
|
+
if (!Number.isFinite(n) || n < 0)
|
|
90
|
+
return null;
|
|
91
|
+
const unit = m[2] ?? "s";
|
|
92
|
+
const mult = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60_000 : 3_600_000;
|
|
93
|
+
return n * mult;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=reboot-marker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reboot-marker.js","sourceRoot":"","sources":["../../src/lib/reboot-marker.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,mEAAmE;AACnE,mEAAmE;AACnE,6DAA6D;AAC7D,qEAAqE;AACrE,sCAAsC;AACtC,EAAE;AACF,sEAAsE;AACtE,kEAAkE;AAClE,4BAA4B;AAE5B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,CAAC,MAAM,mBAAmB,GAAG,mCAAmC,CAAC;AACvE,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAY7C;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAe,mBAAmB,EAClC,MAAY,IAAI,IAAI,EAAE;IAEtB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QAAC,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,IAAI,CAAC;YAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IACpG,oDAAoD;IACpD,IAAI,CAAC;QAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAElC,IAAI,MAAoB,CAAC;IACzB,IAAI,CAAC;QAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IACxD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAChG,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC9C,IAAI,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,IAAI,SAAS,CAAC,OAAO,EAAE,IAAI,GAAG,CAAC,OAAO,EAAE;QAAE,OAAO,IAAI,CAAC,CAAC,QAAQ;IAC/D,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;AACnD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAKjC;IACC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,mBAAmB,CAAC;IAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,cAAc,CAAC;IAC3C,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,CAAC;IAClD,MAAM,IAAI,GAAiB,EAAE,UAAU,EAAE,SAAS,CAAC,WAAW,EAAE,EAAE,CAAC;IACnE,IAAI,IAAI,CAAC,MAAM;QAAE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3C,IAAI,CAAC;QAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAC5E,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC;QAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACxC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC;AAC/C,CAAC;AAED;oCACoC;AACpC,MAAM,UAAU,aAAa,CAAC,CAAS;IACrC,MAAM,CAAC,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACjD,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;IACzB,MAAM,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACzF,OAAO,CAAC,GAAG,IAAI,CAAC;AAClB,CAAC"}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface Snapshot {
|
|
|
27
27
|
systemd?: SystemdData;
|
|
28
28
|
ntp?: NtpData;
|
|
29
29
|
file_descriptors?: FileDescriptorData;
|
|
30
|
+
expected_reboot?: boolean;
|
|
31
|
+
expected_reboot_reason?: string;
|
|
30
32
|
}
|
|
31
33
|
export interface ConntrackData {
|
|
32
34
|
available: boolean;
|
|
@@ -98,6 +100,12 @@ export interface SystemInfo {
|
|
|
98
100
|
hostname: string;
|
|
99
101
|
ip: string;
|
|
100
102
|
os: string;
|
|
103
|
+
/** `ID=` from /etc/os-release, lowercased. e.g. "ubuntu", "debian", "rocky", "arch", "alpine". */
|
|
104
|
+
os_id?: string;
|
|
105
|
+
/** `ID_LIKE=` from /etc/os-release, lowercased, space-separated. Used by Forge
|
|
106
|
+
* to pick distro-family-specific fix command variants. e.g. on Rocky this
|
|
107
|
+
* is "rhel centos fedora"; on Ubuntu it is "debian". */
|
|
108
|
+
os_id_like?: string;
|
|
101
109
|
kernel: string;
|
|
102
110
|
uptime_seconds: number;
|
|
103
111
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, existsSync, writeFileSync, statSync, rmSync, chmodSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
consumeRebootMarker,
|
|
7
|
+
writeRebootMarker,
|
|
8
|
+
parseDuration,
|
|
9
|
+
} from "../lib/reboot-marker.js";
|
|
10
|
+
import { parseCliArgs } from "../cli.js";
|
|
11
|
+
|
|
12
|
+
let tmpDir: string;
|
|
13
|
+
let path: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = mkdtempSync(join(tmpdir(), "crucible-test-"));
|
|
17
|
+
path = join(tmpDir, "reboot-expected");
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("consumeRebootMarker", () => {
|
|
24
|
+
it("7. marker present, not expired: returns flag, deletes file", () => {
|
|
25
|
+
const future = new Date(Date.now() + 5 * 60_000).toISOString();
|
|
26
|
+
writeFileSync(path, JSON.stringify({ expires_at: future, reason: "kernel update" }));
|
|
27
|
+
const out = consumeRebootMarker(path);
|
|
28
|
+
expect(out).toEqual({ expected: true, reason: "kernel update" });
|
|
29
|
+
expect(existsSync(path)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("8. marker present, expired: returns null, deletes file", () => {
|
|
33
|
+
const past = new Date(Date.now() - 60_000).toISOString();
|
|
34
|
+
writeFileSync(path, JSON.stringify({ expires_at: past, reason: "stale" }));
|
|
35
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
36
|
+
expect(existsSync(path)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("9. marker absent: returns null, no throw", () => {
|
|
40
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("15. malformed JSON: returns null, file deleted, no crash", () => {
|
|
44
|
+
writeFileSync(path, "{not json at all");
|
|
45
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
46
|
+
expect(existsSync(path)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("invalid expires_at (missing): returns null, file deleted", () => {
|
|
50
|
+
writeFileSync(path, JSON.stringify({ reason: "oops" }));
|
|
51
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
52
|
+
expect(existsSync(path)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("consumed marker cannot be re-read (single-use)", () => {
|
|
56
|
+
const future = new Date(Date.now() + 60_000).toISOString();
|
|
57
|
+
writeFileSync(path, JSON.stringify({ expires_at: future }));
|
|
58
|
+
expect(consumeRebootMarker(path)).not.toBeNull();
|
|
59
|
+
expect(consumeRebootMarker(path)).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("writeRebootMarker", () => {
|
|
64
|
+
it("13. writes file at given path with correct TTL and reason, 0600 mode", () => {
|
|
65
|
+
const now = new Date("2026-04-21T22:00:00Z");
|
|
66
|
+
const res = writeRebootMarker({ path, reason: "kernel update", ttlMs: 10 * 60_000, now });
|
|
67
|
+
expect(res.path).toBe(path);
|
|
68
|
+
expect(res.expires_at).toBe("2026-04-21T22:10:00.000Z");
|
|
69
|
+
expect(existsSync(path)).toBe(true);
|
|
70
|
+
const mode = statSync(path).mode & 0o777;
|
|
71
|
+
expect(mode).toBe(0o600);
|
|
72
|
+
const round = consumeRebootMarker(path, new Date("2026-04-21T22:05:00Z"));
|
|
73
|
+
expect(round).toEqual({ expected: true, reason: "kernel update" });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("default TTL is 10 minutes", () => {
|
|
77
|
+
const now = new Date("2026-04-21T22:00:00Z");
|
|
78
|
+
const res = writeRebootMarker({ path, now });
|
|
79
|
+
expect(res.expires_at).toBe("2026-04-21T22:10:00.000Z");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("parseDuration", () => {
|
|
84
|
+
it.each([
|
|
85
|
+
["10m", 600_000],
|
|
86
|
+
["2h", 7_200_000],
|
|
87
|
+
["600s", 600_000],
|
|
88
|
+
["500ms", 500],
|
|
89
|
+
["30", 30_000], // bare number -> seconds
|
|
90
|
+
])("%s -> %d ms", (input, ms) => {
|
|
91
|
+
expect(parseDuration(input)).toBe(ms);
|
|
92
|
+
});
|
|
93
|
+
it("rejects garbage", () => {
|
|
94
|
+
expect(parseDuration("forever")).toBeNull();
|
|
95
|
+
expect(parseDuration("-5m")).toBeNull();
|
|
96
|
+
expect(parseDuration("")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("CLI parseCliArgs subcommands", () => {
|
|
101
|
+
it("14. `reboot` subcommand captured with flags", () => {
|
|
102
|
+
const { result } = parseCliArgs(["reboot", "--reason", "kernel update"], "1.0.0");
|
|
103
|
+
expect(result.mode).toBe("reboot");
|
|
104
|
+
expect(result.reason).toBe("kernel update");
|
|
105
|
+
});
|
|
106
|
+
it("`mark-reboot` with --ttl parsed through", () => {
|
|
107
|
+
const { result } = parseCliArgs(["mark-reboot", "--ttl=5m", "--reason=test"], "1.0.0");
|
|
108
|
+
expect(result.mode).toBe("mark-reboot");
|
|
109
|
+
expect(result.ttl).toBe("5m");
|
|
110
|
+
expect(result.reason).toBe("test");
|
|
111
|
+
});
|
|
112
|
+
it("`mark-reboot --help` returns help output without running", () => {
|
|
113
|
+
const { result, output } = parseCliArgs(["mark-reboot", "--help"], "1.0.0");
|
|
114
|
+
expect(result.mode).toBe("help");
|
|
115
|
+
expect(output).toContain("mark-reboot");
|
|
116
|
+
});
|
|
117
|
+
it("top-level help lists the new subcommands", () => {
|
|
118
|
+
const { output } = parseCliArgs(["--help"], "1.0.0");
|
|
119
|
+
expect(output).toMatch(/mark-reboot/);
|
|
120
|
+
expect(output).toMatch(/reboot/);
|
|
121
|
+
});
|
|
122
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
// or collector initialization so --version and --help exit cleanly even when
|
|
3
3
|
// the config file is missing or the host lacks the tools the collectors need.
|
|
4
4
|
|
|
5
|
+
export type CliMode = "version" | "help" | "run" | "mark-reboot" | "reboot";
|
|
6
|
+
|
|
5
7
|
export interface CliArgs {
|
|
6
|
-
mode:
|
|
8
|
+
mode: CliMode;
|
|
7
9
|
configPath: string;
|
|
10
|
+
reason?: string;
|
|
11
|
+
ttl?: string; // raw duration string, parsed by caller
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
export const DEFAULT_CONFIG_PATH = "/etc/glassmkr/collector.yaml";
|
|
@@ -13,6 +17,25 @@ export function parseCliArgs(argv: string[], version: string): { result: CliArgs
|
|
|
13
17
|
// argv is typically process.argv.slice(2)
|
|
14
18
|
let configPath = DEFAULT_CONFIG_PATH;
|
|
15
19
|
|
|
20
|
+
// Subcommand dispatch: `mark-reboot` and `reboot` take their own flags
|
|
21
|
+
// (--reason, --ttl) but re-use --help.
|
|
22
|
+
if (argv[0] === "mark-reboot" || argv[0] === "reboot") {
|
|
23
|
+
const mode: "mark-reboot" | "reboot" = argv[0];
|
|
24
|
+
let reason: string | undefined;
|
|
25
|
+
let ttl: string | undefined;
|
|
26
|
+
for (let i = 1; i < argv.length; i++) {
|
|
27
|
+
const a = argv[i];
|
|
28
|
+
if (a === "--help" || a === "-h") {
|
|
29
|
+
return { result: { mode: "help", configPath: "" }, output: subcommandHelp(mode, version) };
|
|
30
|
+
}
|
|
31
|
+
if (a === "--reason") { reason = argv[++i]; continue; }
|
|
32
|
+
if (a.startsWith("--reason=")) { reason = a.slice("--reason=".length); continue; }
|
|
33
|
+
if (a === "--ttl") { ttl = argv[++i]; continue; }
|
|
34
|
+
if (a.startsWith("--ttl=")) { ttl = a.slice("--ttl=".length); continue; }
|
|
35
|
+
}
|
|
36
|
+
return { result: { mode, configPath: "", reason, ttl }, output: null };
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
for (let i = 0; i < argv.length; i++) {
|
|
17
40
|
const arg = argv[i];
|
|
18
41
|
if (arg === "--version" || arg === "-v") {
|
|
@@ -50,13 +73,40 @@ export function helpText(version: string): string {
|
|
|
50
73
|
"",
|
|
51
74
|
"Usage:",
|
|
52
75
|
" glassmkr-crucible [options]",
|
|
76
|
+
" glassmkr-crucible mark-reboot [--reason TEXT] [--ttl DURATION]",
|
|
77
|
+
" glassmkr-crucible reboot [--reason TEXT] [--ttl DURATION]",
|
|
53
78
|
"",
|
|
54
79
|
"Options:",
|
|
55
80
|
" -v, --version Print version and exit",
|
|
56
81
|
" -h, --help Print this help and exit",
|
|
57
82
|
` -c, --config Path to config file (default: ${DEFAULT_CONFIG_PATH})`,
|
|
58
83
|
"",
|
|
84
|
+
"Subcommands:",
|
|
85
|
+
" mark-reboot Write a planned-reboot marker so the next boot",
|
|
86
|
+
" does not fire `server_rebooted_unexpectedly`.",
|
|
87
|
+
" You run the reboot yourself afterwards.",
|
|
88
|
+
" reboot Write the marker, then invoke `systemctl reboot`.",
|
|
89
|
+
"",
|
|
59
90
|
"Without options, starts the collector daemon using the config file.",
|
|
60
91
|
"Docs: https://github.com/glassmkr/crucible",
|
|
61
92
|
].join("\n");
|
|
62
93
|
}
|
|
94
|
+
|
|
95
|
+
function subcommandHelp(mode: "mark-reboot" | "reboot", version: string): string {
|
|
96
|
+
const action = mode === "reboot"
|
|
97
|
+
? "Write a planned-reboot marker and invoke `systemctl reboot`."
|
|
98
|
+
: "Write a planned-reboot marker; operator triggers the reboot.";
|
|
99
|
+
return [
|
|
100
|
+
`glassmkr-crucible ${mode} - ${action}`,
|
|
101
|
+
"",
|
|
102
|
+
"Usage:",
|
|
103
|
+
` glassmkr-crucible ${mode} [--reason TEXT] [--ttl DURATION]`,
|
|
104
|
+
"",
|
|
105
|
+
"Options:",
|
|
106
|
+
' --reason TEXT Free-text reason (e.g. "kernel update")',
|
|
107
|
+
" --ttl DURATION Expiry window; e.g. 5m, 10m, 1h (default 10m)",
|
|
108
|
+
"",
|
|
109
|
+
`Marker path: /var/lib/crucible/reboot-expected (requires root).`,
|
|
110
|
+
`v${version}`,
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readOsReleaseField } from "../system.js";
|
|
3
|
+
|
|
4
|
+
describe("readOsReleaseField", () => {
|
|
5
|
+
it("parses unquoted Ubuntu values", () => {
|
|
6
|
+
const s = 'NAME="Ubuntu"\nID=ubuntu\nID_LIKE=debian\nVERSION_ID="24.04"';
|
|
7
|
+
expect(readOsReleaseField(s, "ID")).toBe("ubuntu");
|
|
8
|
+
expect(readOsReleaseField(s, "ID_LIKE")).toBe("debian");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("parses quoted RHEL-family values", () => {
|
|
12
|
+
const s = 'NAME="Rocky Linux"\nID="rocky"\nID_LIKE="rhel centos fedora"';
|
|
13
|
+
expect(readOsReleaseField(s, "ID")).toBe("rocky");
|
|
14
|
+
expect(readOsReleaseField(s, "ID_LIKE")).toBe("rhel centos fedora");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("lowercases the result (some distros uppercase their ID)", () => {
|
|
18
|
+
expect(readOsReleaseField("ID=Alpine", "ID")).toBe("alpine");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns undefined for a missing key", () => {
|
|
22
|
+
expect(readOsReleaseField("ID=arch", "ID_LIKE")).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("does not confuse ID with VERSION_ID", () => {
|
|
26
|
+
const s = 'VERSION_ID="24.04"\nID=ubuntu';
|
|
27
|
+
expect(readOsReleaseField(s, "ID")).toBe("ubuntu");
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/collect/system.ts
CHANGED
|
@@ -3,9 +3,18 @@ import { readProcFile } from "../lib/parse.js";
|
|
|
3
3
|
import { run } from "../lib/exec.js";
|
|
4
4
|
import type { SystemInfo } from "../lib/types.js";
|
|
5
5
|
|
|
6
|
+
// Matches KEY=value with optional surrounding double quotes. Handles both
|
|
7
|
+
// `ID=ubuntu` and `ID="rocky"` styles found in the wild.
|
|
8
|
+
export function readOsReleaseField(osRelease: string, key: string): string | undefined {
|
|
9
|
+
const m = osRelease.match(new RegExp(`^${key}=("?)(.+?)\\1$`, "m"));
|
|
10
|
+
return m ? m[2].toLowerCase() : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export async function collectSystem(): Promise<SystemInfo> {
|
|
7
14
|
const osRelease = readProcFile("/etc/os-release") || "";
|
|
8
15
|
const osName = osRelease.match(/PRETTY_NAME="(.+?)"/)?.[1] || "Unknown";
|
|
16
|
+
const os_id = readOsReleaseField(osRelease, "ID");
|
|
17
|
+
const os_id_like = readOsReleaseField(osRelease, "ID_LIKE");
|
|
9
18
|
const kernel = (await run("uname", ["-r"]))?.trim() || "unknown";
|
|
10
19
|
const uptimeRaw = readProcFile("/proc/uptime") || "0";
|
|
11
20
|
const uptimeSeconds = Math.floor(parseFloat(uptimeRaw.split(" ")[0]));
|
|
@@ -15,6 +24,8 @@ export async function collectSystem(): Promise<SystemInfo> {
|
|
|
15
24
|
hostname: hostname(),
|
|
16
25
|
ip,
|
|
17
26
|
os: osName,
|
|
27
|
+
...(os_id ? { os_id } : {}),
|
|
28
|
+
...(os_id_like ? { os_id_like } : {}),
|
|
18
29
|
kernel,
|
|
19
30
|
uptime_seconds: uptimeSeconds,
|
|
20
31
|
};
|
package/src/index.ts
CHANGED
|
@@ -15,14 +15,44 @@ const PKG_VERSION = (() => {
|
|
|
15
15
|
}
|
|
16
16
|
})();
|
|
17
17
|
|
|
18
|
-
// Handle --version
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
// Handle --version, --help, and planned-reboot subcommands before
|
|
19
|
+
// importing collectors, loading config, or starting the Prometheus
|
|
20
|
+
// server. Keeps the CLI responsive even on hosts missing the config
|
|
21
|
+
// file or external tools.
|
|
21
22
|
const { result: cliArgs, output: cliOutput } = parseCliArgs(process.argv.slice(2), PKG_VERSION);
|
|
22
|
-
if (cliArgs.mode
|
|
23
|
+
if (cliArgs.mode === "version" || cliArgs.mode === "help") {
|
|
23
24
|
console.log(cliOutput);
|
|
24
25
|
process.exit(0);
|
|
25
26
|
}
|
|
27
|
+
if (cliArgs.mode === "mark-reboot" || cliArgs.mode === "reboot") {
|
|
28
|
+
const { writeRebootMarker, parseDuration, DEFAULT_TTL_MS } = await import("./lib/reboot-marker.js");
|
|
29
|
+
const ttlMs = cliArgs.ttl ? parseDuration(cliArgs.ttl) : DEFAULT_TTL_MS;
|
|
30
|
+
if (ttlMs === null) {
|
|
31
|
+
console.error(`[mark-reboot] invalid --ttl value: ${cliArgs.ttl}. Use e.g. 10m, 2h, 600s.`);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const { path, expires_at } = writeRebootMarker({
|
|
36
|
+
reason: cliArgs.reason, ttlMs,
|
|
37
|
+
});
|
|
38
|
+
console.log(`[${cliArgs.mode}] marker written: ${path} (expires ${expires_at}${cliArgs.reason ? `, reason: ${cliArgs.reason}` : ""})`);
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
console.error(`[${cliArgs.mode}] failed to write marker: ${err?.message || err}`);
|
|
41
|
+
console.error(` Most likely cause: need root privileges to write under /var/lib/crucible/.`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (cliArgs.mode === "reboot") {
|
|
45
|
+
const { execFileSync } = await import("node:child_process");
|
|
46
|
+
console.log("[reboot] invoking systemctl reboot");
|
|
47
|
+
try {
|
|
48
|
+
execFileSync("systemctl", ["reboot"], { stdio: "inherit" });
|
|
49
|
+
} catch (err: any) {
|
|
50
|
+
console.error(`[reboot] systemctl reboot failed: ${err?.message || err}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
26
56
|
|
|
27
57
|
import { loadConfig } from "./config.js";
|
|
28
58
|
import { checkForUpdates } from "./lib/version-check.js";
|
|
@@ -51,6 +81,17 @@ import { collectSystemd } from "./collect/systemd.js";
|
|
|
51
81
|
import { collectNtp } from "./collect/ntp.js";
|
|
52
82
|
import { collectFileDescriptors } from "./collect/fd.js";
|
|
53
83
|
import type { Snapshot, IpmiInfo } from "./lib/types.js";
|
|
84
|
+
import { consumeRebootMarker, type PlannedReboot } from "./lib/reboot-marker.js";
|
|
85
|
+
|
|
86
|
+
// Consume the planned-reboot marker once at startup. If the operator ran
|
|
87
|
+
// `crucible-agent mark-reboot` / `reboot` before this boot, the marker
|
|
88
|
+
// exists, we flag it on the first snapshot, and we delete the file (so
|
|
89
|
+
// subsequent snapshots don't keep claiming the reboot was planned).
|
|
90
|
+
const plannedRebootFlag: PlannedReboot | null = consumeRebootMarker();
|
|
91
|
+
if (plannedRebootFlag) {
|
|
92
|
+
console.log(`[collector] Planned reboot acknowledged${plannedRebootFlag.reason ? `: ${plannedRebootFlag.reason}` : ""}`);
|
|
93
|
+
}
|
|
94
|
+
let plannedRebootConsumed = false;
|
|
54
95
|
|
|
55
96
|
const config = loadConfig(cliArgs.configPath);
|
|
56
97
|
|
|
@@ -106,6 +147,14 @@ async function collect() {
|
|
|
106
147
|
security: cachedSecurity,
|
|
107
148
|
};
|
|
108
149
|
|
|
150
|
+
// Single-shot: the very first snapshot after a marked reboot carries
|
|
151
|
+
// the flag, subsequent snapshots do not.
|
|
152
|
+
if (plannedRebootFlag && !plannedRebootConsumed) {
|
|
153
|
+
(snapshot as any).expected_reboot = true;
|
|
154
|
+
if (plannedRebootFlag.reason) (snapshot as any).expected_reboot_reason = plannedRebootFlag.reason;
|
|
155
|
+
plannedRebootConsumed = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
109
158
|
// ZFS and I/O errors: collect every cycle (lightweight checks)
|
|
110
159
|
try { snapshot.zfs = await collectZfs() ?? undefined; } catch { /* skip if ZFS not available */ }
|
|
111
160
|
try { snapshot.io_errors = await collectIoErrors() ?? undefined; } catch { /* skip on error */ }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Planned-reboot marker handling.
|
|
2
|
+
//
|
|
3
|
+
// An operator signals "the next reboot is expected, don't page me"
|
|
4
|
+
// by writing a short-lived JSON file to disk BEFORE rebooting. The
|
|
5
|
+
// collector reads and deletes it on agent startup; the first
|
|
6
|
+
// post-boot snapshot then carries `expected_reboot: true` so Forge's
|
|
7
|
+
// unexpected_reboot rule stays quiet.
|
|
8
|
+
//
|
|
9
|
+
// Single-use (deleted on read regardless of validity) and TTL-guarded
|
|
10
|
+
// (default 10 min) so a forgotten marker cannot silence a genuine
|
|
11
|
+
// crash reboot weeks later.
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_MARKER_PATH = "/var/lib/crucible/reboot-expected";
|
|
17
|
+
export const DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
export interface PlannedReboot {
|
|
20
|
+
expected: true;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RebootMarker {
|
|
25
|
+
expires_at: string; // ISO timestamp
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read and delete the marker at `path`. Returns the resolved reboot flag
|
|
31
|
+
* if the file existed, was parseable JSON, and hasn't expired; otherwise
|
|
32
|
+
* returns null. The file is unlinked in every branch where it existed,
|
|
33
|
+
* so a malformed or stale marker is one-shot (can't linger).
|
|
34
|
+
*/
|
|
35
|
+
export function consumeRebootMarker(
|
|
36
|
+
path: string = DEFAULT_MARKER_PATH,
|
|
37
|
+
now: Date = new Date(),
|
|
38
|
+
): PlannedReboot | null {
|
|
39
|
+
if (!existsSync(path)) return null;
|
|
40
|
+
let raw: string;
|
|
41
|
+
try { raw = readFileSync(path, "utf-8"); } catch { try { unlinkSync(path); } catch {} return null; }
|
|
42
|
+
// Always delete after read, regardless of validity.
|
|
43
|
+
try { unlinkSync(path); } catch {}
|
|
44
|
+
|
|
45
|
+
let parsed: RebootMarker;
|
|
46
|
+
try { parsed = JSON.parse(raw); } catch { return null; }
|
|
47
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.expires_at !== "string") return null;
|
|
48
|
+
const expiresAt = new Date(parsed.expires_at);
|
|
49
|
+
if (isNaN(expiresAt.getTime())) return null;
|
|
50
|
+
if (expiresAt.getTime() <= now.getTime()) return null; // stale
|
|
51
|
+
return { expected: true, reason: parsed.reason };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Write a planned-reboot marker. Used by the `mark-reboot` and `reboot`
|
|
56
|
+
* CLI subcommands. `ttlMs` defaults to 10 minutes. Creates the parent
|
|
57
|
+
* directory if needed. Chmod 600 so other users on the host can't read
|
|
58
|
+
* or modify it.
|
|
59
|
+
*/
|
|
60
|
+
export function writeRebootMarker(opts: {
|
|
61
|
+
reason?: string;
|
|
62
|
+
ttlMs?: number;
|
|
63
|
+
path?: string;
|
|
64
|
+
now?: Date;
|
|
65
|
+
}): { path: string; expires_at: string } {
|
|
66
|
+
const path = opts.path ?? DEFAULT_MARKER_PATH;
|
|
67
|
+
const now = opts.now ?? new Date();
|
|
68
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
69
|
+
const expiresAt = new Date(now.getTime() + ttlMs);
|
|
70
|
+
const body: RebootMarker = { expires_at: expiresAt.toISOString() };
|
|
71
|
+
if (opts.reason) body.reason = opts.reason;
|
|
72
|
+
try { mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); } catch {}
|
|
73
|
+
writeFileSync(path, JSON.stringify(body), { mode: 0o600 });
|
|
74
|
+
try { chmodSync(path, 0o600); } catch {}
|
|
75
|
+
return { path, expires_at: body.expires_at };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Parse a duration like "10m", "2h", "600s" into milliseconds. Used by
|
|
79
|
+
* the CLI for the `--ttl` flag. */
|
|
80
|
+
export function parseDuration(s: string): number | null {
|
|
81
|
+
const m = /^(\d+)\s*(ms|s|m|h)?$/.exec(s.trim());
|
|
82
|
+
if (!m) return null;
|
|
83
|
+
const n = parseInt(m[1], 10);
|
|
84
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
85
|
+
const unit = m[2] ?? "s";
|
|
86
|
+
const mult = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60_000 : 3_600_000;
|
|
87
|
+
return n * mult;
|
|
88
|
+
}
|
package/src/lib/types.ts
CHANGED
|
@@ -18,6 +18,12 @@ export interface Snapshot {
|
|
|
18
18
|
systemd?: SystemdData;
|
|
19
19
|
ntp?: NtpData;
|
|
20
20
|
file_descriptors?: FileDescriptorData;
|
|
21
|
+
// Planned-reboot flag: set only on the first snapshot after a reboot
|
|
22
|
+
// that was marked with `crucible-agent mark-reboot` / `reboot`. Forge
|
|
23
|
+
// reads this to suppress the `unexpected_reboot` rule. Single-use:
|
|
24
|
+
// subsequent snapshots don't carry it.
|
|
25
|
+
expected_reboot?: boolean;
|
|
26
|
+
expected_reboot_reason?: string;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export interface ConntrackData {
|
|
@@ -73,6 +79,12 @@ export interface SystemInfo {
|
|
|
73
79
|
hostname: string;
|
|
74
80
|
ip: string;
|
|
75
81
|
os: string;
|
|
82
|
+
/** `ID=` from /etc/os-release, lowercased. e.g. "ubuntu", "debian", "rocky", "arch", "alpine". */
|
|
83
|
+
os_id?: string;
|
|
84
|
+
/** `ID_LIKE=` from /etc/os-release, lowercased, space-separated. Used by Forge
|
|
85
|
+
* to pick distro-family-specific fix command variants. e.g. on Rocky this
|
|
86
|
+
* is "rhel centos fedora"; on Ubuntu it is "debian". */
|
|
87
|
+
os_id_like?: string;
|
|
76
88
|
kernel: string;
|
|
77
89
|
uptime_seconds: number;
|
|
78
90
|
}
|