wraith-cli 1.6.0__tar.gz → 1.7.0__tar.gz
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.
- wraith_cli-1.7.0/.claude/settings.local.json +13 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/CHANGELOG.md +2 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/PKG-INFO +27 -4
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/README.md +26 -3
- wraith_cli-1.7.0/docs/nas.md +88 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/docs/usage.md +224 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/mkdocs.yml +1 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/pyproject.toml +1 -1
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/src/wraith_cli/config.py +10 -0
- wraith_cli-1.7.0/src/wraith_cli/doctor.py +216 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/src/wraith_cli/main.py +311 -1
- wraith_cli-1.7.0/src/wraith_cli/nas.py +353 -0
- wraith_cli-1.7.0/src/wraith_cli/notify.py +35 -0
- wraith_cli-1.7.0/src/wraith_cli/report.py +85 -0
- wraith_cli-1.7.0/src/wraith_cli/shares.py +98 -0
- wraith_cli-1.7.0/src/wraith_cli/snapshots.py +133 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/tests/test_cli.py +9 -3
- wraith_cli-1.7.0/tests/test_doctor.py +238 -0
- wraith_cli-1.7.0/tests/test_nas.py +366 -0
- wraith_cli-1.7.0/tests/test_nas_cli.py +280 -0
- wraith_cli-1.7.0/tests/test_notify.py +51 -0
- wraith_cli-1.7.0/tests/test_report.py +94 -0
- wraith_cli-1.7.0/tests/test_shares.py +112 -0
- wraith_cli-1.7.0/tests/test_snapshots.py +157 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/uv.lock +1 -1
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.cz.yaml +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.env.example +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.gitea/CODEOWNERS.md +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.gitea/PULL_REQUEST_TEMPLATE.md +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.gitea/workflows/pages.yml +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.gitea/workflows/release.yml +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.gitea/workflows/test.yml +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.gitignore +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.pre-commit-config.yaml +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/.secrets.baseline +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/CONTRIBUTING.md +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/LICENSE +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/bin/build.sh +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/bin/run_tests.sh +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/bin/setup_venv.sh +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/docs/architecture.md +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/docs/index.md +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/docs/reference.md +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/docs/security.md +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/src/wraith_cli/__init__.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/src/wraith_cli/assets.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/src/wraith_cli/providers.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/src/wraith_cli/qol.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/src/wraith_cli/repo_make.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/src/wraith_cli/shield.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/tests/conftest.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/tests/test_config.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/tests/test_providers.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/tests/test_qol.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/tests/test_repo_make.py +0 -0
- {wraith_cli-1.6.0 → wraith_cli-1.7.0}/tests/test_shield.py +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(uv run *)",
|
|
5
|
+
"Bash(./bin/run_tests.sh)",
|
|
6
|
+
"Bash(git add *)",
|
|
7
|
+
"Bash(git commit -m '👻 feature/TJP-12062: add enterprise NAS management suite *)",
|
|
8
|
+
"Bash(python3 -)",
|
|
9
|
+
"Bash(git -C /home/thomaspeoples/pjt-nas/nas/SovereignDocs/gitea-repos status --short)",
|
|
10
|
+
"Bash(git -C /home/thomaspeoples/pjt-nas/nas/SovereignDocs/gitea-repos branch --show-current)"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wraith-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: Sovereign Command Centre for a Ghost Stack
|
|
5
5
|
Project-URL: Homepage, https://git.thomaspeoples.com/thomaspeoples/wraith-cli
|
|
6
6
|
Project-URL: Documentation, https://www.thomaspeoples.com/gitea-repos/wraith-cli/
|
|
@@ -60,12 +60,35 @@ Description-Content-Type: text/markdown
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
# 👻 Wraith-CLI
|
|
63
|
-
### *Sovereign Orchestration for the Ghost Stack*
|
|
63
|
+
### *Sovereign Orchestration & NAS Management for the Ghost Stack*
|
|
64
64
|
|
|
65
65
|
**Wraith-CLI** is the nervous system of the Ghost Stack. It bridges the gap between
|
|
66
|
-
high-level container orchestration and bare-metal reality
|
|
67
|
-
|
|
66
|
+
high-level container orchestration and bare-metal reality — instant project scaffolding,
|
|
67
|
+
container operations, and a full NAS management suite in one binary.
|
|
68
68
|
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🗄️ NAS Management Suite
|
|
72
|
+
|
|
73
|
+
| Command | Purpose |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `wraith disks` | Physical disk inventory — model, serial, capacity, HDD/SSD, bus |
|
|
76
|
+
| `wraith storage` | Filesystem usage with configurable warn/crit thresholds |
|
|
77
|
+
| `wraith raid` | mdraid array and ZFS pool health |
|
|
78
|
+
| `wraith smart /dev/sda` | SMART verdict + failure-predicting attributes per drive |
|
|
79
|
+
| `wraith shares` | NFS exports and Samba shares, guest access flagged |
|
|
80
|
+
| `wraith snap` | ZFS/btrfs snapshot create / list / prune (safe by design) |
|
|
81
|
+
| `wraith doctor` | Full health check suite — cron-friendly exit codes, webhook/ntfy alerts |
|
|
82
|
+
| `wraith report` | Timestamped markdown/JSON audit reports |
|
|
83
|
+
|
|
84
|
+
Every read-only command takes `--json` — pipe Wraith straight into dashboards,
|
|
85
|
+
monitoring stacks, or fleet tooling. Unattended monitoring is one cron line:
|
|
86
|
+
|
|
87
|
+
```cron
|
|
88
|
+
*/30 * * * * wraith doctor --notify
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
See the [NAS Management docs](docs/nas.md) for full coverage.
|
|
69
92
|
|
|
70
93
|
---
|
|
71
94
|
|
|
@@ -5,12 +5,35 @@
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
# 👻 Wraith-CLI
|
|
8
|
-
### *Sovereign Orchestration for the Ghost Stack*
|
|
8
|
+
### *Sovereign Orchestration & NAS Management for the Ghost Stack*
|
|
9
9
|
|
|
10
10
|
**Wraith-CLI** is the nervous system of the Ghost Stack. It bridges the gap between
|
|
11
|
-
high-level container orchestration and bare-metal reality
|
|
12
|
-
|
|
11
|
+
high-level container orchestration and bare-metal reality — instant project scaffolding,
|
|
12
|
+
container operations, and a full NAS management suite in one binary.
|
|
13
13
|
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🗄️ NAS Management Suite
|
|
17
|
+
|
|
18
|
+
| Command | Purpose |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `wraith disks` | Physical disk inventory — model, serial, capacity, HDD/SSD, bus |
|
|
21
|
+
| `wraith storage` | Filesystem usage with configurable warn/crit thresholds |
|
|
22
|
+
| `wraith raid` | mdraid array and ZFS pool health |
|
|
23
|
+
| `wraith smart /dev/sda` | SMART verdict + failure-predicting attributes per drive |
|
|
24
|
+
| `wraith shares` | NFS exports and Samba shares, guest access flagged |
|
|
25
|
+
| `wraith snap` | ZFS/btrfs snapshot create / list / prune (safe by design) |
|
|
26
|
+
| `wraith doctor` | Full health check suite — cron-friendly exit codes, webhook/ntfy alerts |
|
|
27
|
+
| `wraith report` | Timestamped markdown/JSON audit reports |
|
|
28
|
+
|
|
29
|
+
Every read-only command takes `--json` — pipe Wraith straight into dashboards,
|
|
30
|
+
monitoring stacks, or fleet tooling. Unattended monitoring is one cron line:
|
|
31
|
+
|
|
32
|
+
```cron
|
|
33
|
+
*/30 * * * * wraith doctor --notify
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
See the [NAS Management docs](docs/nas.md) for full coverage.
|
|
14
37
|
|
|
15
38
|
---
|
|
16
39
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# NAS Management
|
|
2
|
+
|
|
3
|
+
Wraith turns your Ghost Stack node into a fully managed NAS. The
|
|
4
|
+
suite covers hardware introspection, capacity monitoring, share
|
|
5
|
+
discovery, snapshot lifecycle, and automated health enforcement —
|
|
6
|
+
all from one binary, all scriptable via `--json`.
|
|
7
|
+
|
|
8
|
+
## Command Overview
|
|
9
|
+
|
|
10
|
+
| Command | What it does |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `wraith disks` | Physical disk inventory: model, serial, capacity, HDD/SSD, bus |
|
|
13
|
+
| `wraith storage` | Filesystem usage with warn/crit thresholds |
|
|
14
|
+
| `wraith raid` | mdraid array and ZFS pool health |
|
|
15
|
+
| `wraith smart <device>` | Per-drive SMART verdict and failure-predicting attributes |
|
|
16
|
+
| `wraith shares` | NFS exports and Samba shares, guest-access flagged |
|
|
17
|
+
| `wraith snap` | ZFS/btrfs snapshot list / create / prune |
|
|
18
|
+
| `wraith doctor` | Full health check suite with exit codes and alerting |
|
|
19
|
+
| `wraith report` | Point-in-time audit report (markdown or JSON) |
|
|
20
|
+
|
|
21
|
+
Every read-only command accepts `--json` for machine-readable
|
|
22
|
+
output, making Wraith a drop-in data source for dashboards,
|
|
23
|
+
monitoring pipelines, and fleet tooling.
|
|
24
|
+
|
|
25
|
+
## The Doctor
|
|
26
|
+
|
|
27
|
+
`wraith doctor` is the heartbeat of the system. It checks:
|
|
28
|
+
|
|
29
|
+
- Docker daemon responsiveness
|
|
30
|
+
- Tailscale mesh connectivity
|
|
31
|
+
- Storage utilisation against configured thresholds
|
|
32
|
+
- RAID array and ZFS pool degradation
|
|
33
|
+
- SMART self-assessment on every physical disk
|
|
34
|
+
- HTTP health of every configured stack service
|
|
35
|
+
|
|
36
|
+
It exits non-zero when anything is critical, so it slots straight
|
|
37
|
+
into cron:
|
|
38
|
+
|
|
39
|
+
```cron
|
|
40
|
+
*/30 * * * * wraith doctor --notify || logger "NAS degraded"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
With `--notify`, warn/crit findings are pushed to your configured
|
|
44
|
+
webhook or [ntfy](https://ntfy.sh) topic.
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
Add to `.wraith.yaml`:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
storage_warn_pct: 80
|
|
52
|
+
storage_crit_pct: 90
|
|
53
|
+
alerts:
|
|
54
|
+
webhook_url: https://ntfy.sh/my-nas-alerts
|
|
55
|
+
kind: ntfy # or "webhook" for a JSON POST
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Snapshots
|
|
59
|
+
|
|
60
|
+
Snapshot lifecycle management with a safety guarantee: `snap prune`
|
|
61
|
+
only ever considers snapshots Wraith itself created (the `wraith-`
|
|
62
|
+
prefix). Manual snapshots are never touched.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
wraith snap create tank/data # ZFS, auto-named
|
|
66
|
+
wraith snap create /srv/data --btrfs-dest /srv/.snapshots/nightly
|
|
67
|
+
wraith snap list tank/data
|
|
68
|
+
wraith snap prune tank/data --keep 7 # confirm before destroy
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A nightly rotation is two cron lines:
|
|
72
|
+
|
|
73
|
+
```cron
|
|
74
|
+
0 2 * * * wraith snap create tank/data
|
|
75
|
+
30 2 * * * wraith snap prune tank/data --keep 14 --yes
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Audit Reports
|
|
79
|
+
|
|
80
|
+
`wraith report` captures disks, storage, RAID state, and the full
|
|
81
|
+
doctor suite into a timestamped document — an audit trail for
|
|
82
|
+
compliance, capacity planning, or a paper trail before and after
|
|
83
|
+
hardware changes.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
wraith report # markdown, timestamped name
|
|
87
|
+
wraith report -f json -o nightly.json # feed it to anything
|
|
88
|
+
```
|
|
@@ -28,6 +28,14 @@ $ [OPTIONS] COMMAND [ARGS]...
|
|
|
28
28
|
* `logs`: Unified hardware health and logging portal.
|
|
29
29
|
* `mesh`: Map the Tailscale Ghost Mesh network.
|
|
30
30
|
* `audit`: Execute a Sovereign security and...
|
|
31
|
+
* `disks`: Inventory the physical disks in this NAS.
|
|
32
|
+
* `storage`: Show filesystem usage across all mounted...
|
|
33
|
+
* `raid`: Report RAID array and ZFS pool health.
|
|
34
|
+
* `smart`: Deep-dive SMART health for a single drive.
|
|
35
|
+
* `shares`: List network shares exposed by this NAS.
|
|
36
|
+
* `doctor`: Run the full NAS health check suite.
|
|
37
|
+
* `report`: Generate a point-in-time NAS health report.
|
|
38
|
+
* `snap`: Manage ZFS/btrfs snapshots.
|
|
31
39
|
|
|
32
40
|
## `init`
|
|
33
41
|
|
|
@@ -243,3 +251,219 @@ $ audit [OPTIONS]
|
|
|
243
251
|
**Options**:
|
|
244
252
|
|
|
245
253
|
* `--help`: Show this message and exit.
|
|
254
|
+
|
|
255
|
+
## `disks`
|
|
256
|
+
|
|
257
|
+
Inventory the physical disks in this NAS.
|
|
258
|
+
|
|
259
|
+
Enumerates block devices with model, serial, capacity,
|
|
260
|
+
media type (HDD/SSD), bus, and active mountpoints.
|
|
261
|
+
|
|
262
|
+
**Usage**:
|
|
263
|
+
|
|
264
|
+
```console
|
|
265
|
+
$ disks [OPTIONS]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Options**:
|
|
269
|
+
|
|
270
|
+
* `--json`: Emit machine-readable JSON.
|
|
271
|
+
* `--help`: Show this message and exit.
|
|
272
|
+
|
|
273
|
+
## `storage`
|
|
274
|
+
|
|
275
|
+
Show filesystem usage across all mounted volumes.
|
|
276
|
+
|
|
277
|
+
Colour-codes utilisation against the configured warn/crit
|
|
278
|
+
thresholds (storage_warn_pct / storage_crit_pct).
|
|
279
|
+
|
|
280
|
+
**Usage**:
|
|
281
|
+
|
|
282
|
+
```console
|
|
283
|
+
$ storage [OPTIONS]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Options**:
|
|
287
|
+
|
|
288
|
+
* `--json`: Emit machine-readable JSON.
|
|
289
|
+
* `--help`: Show this message and exit.
|
|
290
|
+
|
|
291
|
+
## `raid`
|
|
292
|
+
|
|
293
|
+
Report RAID array and ZFS pool health.
|
|
294
|
+
|
|
295
|
+
Parses /proc/mdstat for Linux software RAID and queries
|
|
296
|
+
zpool for ZFS pool state. Degraded arrays surface in red.
|
|
297
|
+
|
|
298
|
+
**Usage**:
|
|
299
|
+
|
|
300
|
+
```console
|
|
301
|
+
$ raid [OPTIONS]
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Options**:
|
|
305
|
+
|
|
306
|
+
* `--json`: Emit machine-readable JSON.
|
|
307
|
+
* `--help`: Show this message and exit.
|
|
308
|
+
|
|
309
|
+
## `smart`
|
|
310
|
+
|
|
311
|
+
Deep-dive SMART health for a single drive.
|
|
312
|
+
|
|
313
|
+
Pulls the self-assessment verdict, temperature, power-on
|
|
314
|
+
hours, and failure-predicting attributes via smartctl.
|
|
315
|
+
|
|
316
|
+
**Usage**:
|
|
317
|
+
|
|
318
|
+
```console
|
|
319
|
+
$ smart [OPTIONS] DEVICE
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Arguments**:
|
|
323
|
+
|
|
324
|
+
* `DEVICE`: Block device path (e.g. /dev/sda) [required]
|
|
325
|
+
|
|
326
|
+
**Options**:
|
|
327
|
+
|
|
328
|
+
* `--json`: Emit machine-readable JSON.
|
|
329
|
+
* `--help`: Show this message and exit.
|
|
330
|
+
|
|
331
|
+
## `shares`
|
|
332
|
+
|
|
333
|
+
List network shares exposed by this NAS.
|
|
334
|
+
|
|
335
|
+
Discovers NFS exports (/etc/exports) and Samba shares
|
|
336
|
+
(smb.conf), flagging guest-accessible SMB shares.
|
|
337
|
+
|
|
338
|
+
**Usage**:
|
|
339
|
+
|
|
340
|
+
```console
|
|
341
|
+
$ shares [OPTIONS]
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Options**:
|
|
345
|
+
|
|
346
|
+
* `--json`: Emit machine-readable JSON.
|
|
347
|
+
* `--help`: Show this message and exit.
|
|
348
|
+
|
|
349
|
+
## `doctor`
|
|
350
|
+
|
|
351
|
+
Run the full NAS health check suite.
|
|
352
|
+
|
|
353
|
+
Checks Docker, Tailscale, storage thresholds, RAID/pool
|
|
354
|
+
health, per-disk SMART status, and service endpoints.
|
|
355
|
+
Exits non-zero on any critical finding — cron-friendly.
|
|
356
|
+
|
|
357
|
+
**Usage**:
|
|
358
|
+
|
|
359
|
+
```console
|
|
360
|
+
$ doctor [OPTIONS]
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Options**:
|
|
364
|
+
|
|
365
|
+
* `--json`: Emit machine-readable JSON.
|
|
366
|
+
* `--notify`: Send warn/crit findings to the configured webhook.
|
|
367
|
+
* `--help`: Show this message and exit.
|
|
368
|
+
|
|
369
|
+
## `report`
|
|
370
|
+
|
|
371
|
+
Generate a point-in-time NAS health report.
|
|
372
|
+
|
|
373
|
+
Captures disks, storage, RAID state, and the full doctor
|
|
374
|
+
check suite into a timestamped markdown or JSON document —
|
|
375
|
+
an audit trail for compliance and capacity planning.
|
|
376
|
+
|
|
377
|
+
**Usage**:
|
|
378
|
+
|
|
379
|
+
```console
|
|
380
|
+
$ report [OPTIONS]
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Options**:
|
|
384
|
+
|
|
385
|
+
* `-o, --output PATH`: Report file destination.
|
|
386
|
+
* `-f, --format TEXT`: Report format: md or json. [default: md]
|
|
387
|
+
* `--help`: Show this message and exit.
|
|
388
|
+
|
|
389
|
+
## `snap`
|
|
390
|
+
|
|
391
|
+
Manage ZFS/btrfs snapshots.
|
|
392
|
+
|
|
393
|
+
**Usage**:
|
|
394
|
+
|
|
395
|
+
```console
|
|
396
|
+
$ snap [OPTIONS] COMMAND [ARGS]...
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Options**:
|
|
400
|
+
|
|
401
|
+
* `--help`: Show this message and exit.
|
|
402
|
+
|
|
403
|
+
**Commands**:
|
|
404
|
+
|
|
405
|
+
* `list`: List ZFS snapshots, newest last.
|
|
406
|
+
* `create`: Create a snapshot of a ZFS dataset or...
|
|
407
|
+
* `prune`: Prune old wraith-created snapshots from a...
|
|
408
|
+
|
|
409
|
+
### `snap list`
|
|
410
|
+
|
|
411
|
+
List ZFS snapshots, newest last.
|
|
412
|
+
|
|
413
|
+
**Usage**:
|
|
414
|
+
|
|
415
|
+
```console
|
|
416
|
+
$ snap list [OPTIONS] [DATASET]
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Arguments**:
|
|
420
|
+
|
|
421
|
+
* `[DATASET]`: Limit to one ZFS dataset.
|
|
422
|
+
|
|
423
|
+
**Options**:
|
|
424
|
+
|
|
425
|
+
* `--json`: Emit machine-readable JSON.
|
|
426
|
+
* `--help`: Show this message and exit.
|
|
427
|
+
|
|
428
|
+
### `snap create`
|
|
429
|
+
|
|
430
|
+
Create a snapshot of a ZFS dataset or btrfs subvolume.
|
|
431
|
+
|
|
432
|
+
**Usage**:
|
|
433
|
+
|
|
434
|
+
```console
|
|
435
|
+
$ snap create [OPTIONS] TARGET
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Arguments**:
|
|
439
|
+
|
|
440
|
+
* `TARGET`: ZFS dataset (pool/data) or btrfs subvolume path. [required]
|
|
441
|
+
|
|
442
|
+
**Options**:
|
|
443
|
+
|
|
444
|
+
* `-n, --name TEXT`: Snapshot name override.
|
|
445
|
+
* `--btrfs-dest TEXT`: Treat target as btrfs subvolume; snapshot here.
|
|
446
|
+
* `--help`: Show this message and exit.
|
|
447
|
+
|
|
448
|
+
### `snap prune`
|
|
449
|
+
|
|
450
|
+
Prune old wraith-created snapshots from a dataset.
|
|
451
|
+
|
|
452
|
+
Only snapshots named with the wraith- prefix are candidates;
|
|
453
|
+
manually created snapshots are never touched.
|
|
454
|
+
|
|
455
|
+
**Usage**:
|
|
456
|
+
|
|
457
|
+
```console
|
|
458
|
+
$ snap prune [OPTIONS] DATASET
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**Arguments**:
|
|
462
|
+
|
|
463
|
+
* `DATASET`: ZFS dataset to prune. [required]
|
|
464
|
+
|
|
465
|
+
**Options**:
|
|
466
|
+
|
|
467
|
+
* `-k, --keep INTEGER`: Snapshots to retain. [default: 7]
|
|
468
|
+
* `-y, --yes`: Skip confirmation prompt.
|
|
469
|
+
* `--help`: Show this message and exit.
|
|
@@ -17,6 +17,11 @@ class TemplateModel(BaseModel):
|
|
|
17
17
|
url: str
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class AlertsModel(BaseModel):
|
|
21
|
+
webhook_url: str | None = None
|
|
22
|
+
kind: str = "webhook" # "webhook" (JSON POST) or "ntfy" (text POST)
|
|
23
|
+
|
|
24
|
+
|
|
20
25
|
class WraithSettings(BaseSettings):
|
|
21
26
|
stack_name: str = "Ghost Stack"
|
|
22
27
|
viking_api: HttpUrl | None = None
|
|
@@ -25,6 +30,11 @@ class WraithSettings(BaseSettings):
|
|
|
25
30
|
docker_enabled: bool = False
|
|
26
31
|
tailscale_enabled: bool = False
|
|
27
32
|
|
|
33
|
+
# NAS management
|
|
34
|
+
alerts: AlertsModel = Field(default_factory=AlertsModel)
|
|
35
|
+
storage_warn_pct: int = 80
|
|
36
|
+
storage_crit_pct: int = 90
|
|
37
|
+
|
|
28
38
|
# Provider Settings
|
|
29
39
|
forge_api_url: str | None = None
|
|
30
40
|
forge_token: str | None = None
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Full-stack NAS health check engine.
|
|
2
|
+
|
|
3
|
+
Each check returns {"name", "status", "detail"} where status is one
|
|
4
|
+
of: ok, warn, crit, skip. The CLI maps any crit to a non-zero exit
|
|
5
|
+
code so `wraith doctor` slots straight into cron and CI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from wraith_cli import nas
|
|
16
|
+
from wraith_cli.config import WraithSettings
|
|
17
|
+
|
|
18
|
+
OK = "ok"
|
|
19
|
+
WARN = "warn"
|
|
20
|
+
CRIT = "crit"
|
|
21
|
+
SKIP = "skip"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _check(name: str, status: str, detail: str) -> dict[str, str]:
|
|
25
|
+
return {"name": name, "status": status, "detail": detail}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_docker(settings: WraithSettings) -> dict[str, str]:
|
|
29
|
+
if not settings.docker_enabled:
|
|
30
|
+
return _check("docker", SKIP, "Disabled in config")
|
|
31
|
+
result = subprocess.run(
|
|
32
|
+
["docker", "info"], capture_output=True, check=False
|
|
33
|
+
)
|
|
34
|
+
if result.returncode == 0:
|
|
35
|
+
return _check("docker", OK, "Daemon responsive")
|
|
36
|
+
return _check("docker", CRIT, "Docker daemon unreachable")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_tailscale(settings: WraithSettings) -> dict[str, str]:
|
|
40
|
+
if not settings.tailscale_enabled:
|
|
41
|
+
return _check("tailscale", SKIP, "Disabled in config")
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
["tailscale", "status"], capture_output=True, check=False
|
|
44
|
+
)
|
|
45
|
+
if result.returncode == 0:
|
|
46
|
+
return _check("tailscale", OK, "Mesh connected")
|
|
47
|
+
return _check("tailscale", CRIT, "Tailscale down — mesh unreachable")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_storage(settings: WraithSettings) -> list[dict[str, str]]:
|
|
51
|
+
try:
|
|
52
|
+
usage = nas.get_storage(
|
|
53
|
+
settings.storage_warn_pct, settings.storage_crit_pct
|
|
54
|
+
)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return [_check("storage", WARN, f"Could not read usage: {e}")]
|
|
57
|
+
|
|
58
|
+
checks = []
|
|
59
|
+
for u in usage:
|
|
60
|
+
status = {"ok": OK, "warn": WARN, "crit": CRIT}[u["level"]]
|
|
61
|
+
checks.append(
|
|
62
|
+
_check(
|
|
63
|
+
f"storage {u['mount']}",
|
|
64
|
+
status,
|
|
65
|
+
f"{u['used_pct']}% used, "
|
|
66
|
+
f"{nas.human_bytes(u['free_bytes'])} free",
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
return checks or [_check("storage", WARN, "No filesystems found")]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def check_raid() -> list[dict[str, str]]:
|
|
73
|
+
try:
|
|
74
|
+
raid = nas.get_raid_status()
|
|
75
|
+
except Exception as e:
|
|
76
|
+
return [_check("raid", WARN, f"Could not read RAID state: {e}")]
|
|
77
|
+
|
|
78
|
+
checks = []
|
|
79
|
+
for arr in raid["mdraid"]:
|
|
80
|
+
if arr["healthy"]:
|
|
81
|
+
checks.append(
|
|
82
|
+
_check(f"mdraid {arr['name']}", OK, arr["detail"] or "active")
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
checks.append(
|
|
86
|
+
_check(
|
|
87
|
+
f"mdraid {arr['name']}",
|
|
88
|
+
CRIT,
|
|
89
|
+
f"DEGRADED: {arr['detail']}",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
for pool in raid["zpools"]:
|
|
93
|
+
status = OK if pool["healthy"] else CRIT
|
|
94
|
+
checks.append(_check(f"zpool {pool['name']}", status, pool["health"]))
|
|
95
|
+
return checks or [_check("raid", SKIP, "No arrays or pools")]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def check_smart() -> list[dict[str, str]]:
|
|
99
|
+
if shutil.which("smartctl") is None:
|
|
100
|
+
return [_check("smart", SKIP, "smartmontools not installed")]
|
|
101
|
+
try:
|
|
102
|
+
disks = nas.get_disks()
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return [_check("smart", WARN, f"Disk enumeration failed: {e}")]
|
|
105
|
+
|
|
106
|
+
checks = []
|
|
107
|
+
for disk in disks:
|
|
108
|
+
try:
|
|
109
|
+
smart = nas.get_smart(disk["path"])
|
|
110
|
+
except Exception as e:
|
|
111
|
+
checks.append(_check(f"smart {disk['path']}", WARN, str(e)))
|
|
112
|
+
continue
|
|
113
|
+
bad_attrs = {
|
|
114
|
+
k: v
|
|
115
|
+
for k, v in smart["attributes"].items()
|
|
116
|
+
if k != "Available Spare %" and v
|
|
117
|
+
}
|
|
118
|
+
if not smart["passed"]:
|
|
119
|
+
checks.append(
|
|
120
|
+
_check(
|
|
121
|
+
f"smart {disk['path']}",
|
|
122
|
+
CRIT,
|
|
123
|
+
"SMART self-assessment FAILED",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
elif bad_attrs:
|
|
127
|
+
detail = ", ".join(f"{k}={v}" for k, v in bad_attrs.items())
|
|
128
|
+
checks.append(_check(f"smart {disk['path']}", WARN, detail))
|
|
129
|
+
else:
|
|
130
|
+
checks.append(_check(f"smart {disk['path']}", OK, "Healthy"))
|
|
131
|
+
return checks or [_check("smart", SKIP, "No disks found")]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def check_services(settings: WraithSettings) -> list[dict[str, str]]:
|
|
135
|
+
if not settings.services:
|
|
136
|
+
return [_check("services", SKIP, "No services configured")]
|
|
137
|
+
checks = []
|
|
138
|
+
for name, svc in settings.services.items():
|
|
139
|
+
if not svc.health_url:
|
|
140
|
+
checks.append(_check(f"service {name}", SKIP, "No health_url"))
|
|
141
|
+
continue
|
|
142
|
+
try:
|
|
143
|
+
resp = requests.get(str(svc.health_url), timeout=5)
|
|
144
|
+
if resp.status_code == 200:
|
|
145
|
+
checks.append(_check(f"service {name}", OK, "HTTP 200"))
|
|
146
|
+
else:
|
|
147
|
+
checks.append(
|
|
148
|
+
_check(
|
|
149
|
+
f"service {name}",
|
|
150
|
+
WARN,
|
|
151
|
+
f"HTTP {resp.status_code}",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
except requests.exceptions.RequestException:
|
|
155
|
+
checks.append(_check(f"service {name}", CRIT, "Unreachable"))
|
|
156
|
+
return checks
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def run_doctor(settings: WraithSettings) -> dict[str, Any]:
|
|
160
|
+
"""Run the full check suite and summarise."""
|
|
161
|
+
checks: list[dict[str, str]] = []
|
|
162
|
+
checks.append(check_docker(settings))
|
|
163
|
+
checks.append(check_tailscale(settings))
|
|
164
|
+
checks.extend(check_storage(settings))
|
|
165
|
+
checks.extend(check_raid())
|
|
166
|
+
checks.extend(check_smart())
|
|
167
|
+
checks.extend(check_services(settings))
|
|
168
|
+
|
|
169
|
+
counts = {OK: 0, WARN: 0, CRIT: 0, SKIP: 0}
|
|
170
|
+
for c in checks:
|
|
171
|
+
counts[c["status"]] += 1
|
|
172
|
+
|
|
173
|
+
if counts[CRIT]:
|
|
174
|
+
verdict = CRIT
|
|
175
|
+
elif counts[WARN]:
|
|
176
|
+
verdict = WARN
|
|
177
|
+
else:
|
|
178
|
+
verdict = OK
|
|
179
|
+
|
|
180
|
+
return {"verdict": verdict, "counts": counts, "checks": checks}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def render_doctor(result: dict[str, Any]) -> Table:
|
|
184
|
+
styles = {
|
|
185
|
+
OK: "[green]OK[/green]",
|
|
186
|
+
WARN: "[yellow]WARN[/yellow]",
|
|
187
|
+
CRIT: "[red]CRIT[/red]",
|
|
188
|
+
SKIP: "[bright_black]SKIP[/bright_black]",
|
|
189
|
+
}
|
|
190
|
+
counts = result["counts"]
|
|
191
|
+
title = (
|
|
192
|
+
f"Wraith Doctor — {counts[OK]} ok, {counts[WARN]} warn, "
|
|
193
|
+
f"{counts[CRIT]} crit, {counts[SKIP]} skipped"
|
|
194
|
+
)
|
|
195
|
+
table = Table(
|
|
196
|
+
title=title,
|
|
197
|
+
border_style="bright_black",
|
|
198
|
+
header_style="bold green",
|
|
199
|
+
)
|
|
200
|
+
table.add_column("Check", style="cyan")
|
|
201
|
+
table.add_column("Status")
|
|
202
|
+
table.add_column("Detail")
|
|
203
|
+
|
|
204
|
+
for c in result["checks"]:
|
|
205
|
+
table.add_row(c["name"], styles[c["status"]], c["detail"])
|
|
206
|
+
return table
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def summarise_failures(result: dict[str, Any]) -> str:
|
|
210
|
+
"""One-line-per-failure summary for alert payloads."""
|
|
211
|
+
lines = [
|
|
212
|
+
f"[{c['status'].upper()}] {c['name']}: {c['detail']}"
|
|
213
|
+
for c in result["checks"]
|
|
214
|
+
if c["status"] in (WARN, CRIT)
|
|
215
|
+
]
|
|
216
|
+
return "\n".join(lines)
|