wtftools 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wtftools/__init__.py +55 -0
- wtftools/__main__.py +10 -0
- wtftools/audit.py +809 -0
- wtftools/colors.py +111 -0
- wtftools/config.py +249 -0
- wtftools/cron.py +388 -0
- wtftools/events.py +220 -0
- wtftools/explain.py +290 -0
- wtftools/info.py +90 -0
- wtftools/llm.py +129 -0
- wtftools/main.py +1328 -0
- wtftools/snapshot.py +203 -0
- wtftools/sysinfo.py +1608 -0
- wtftools-0.0.0.data/data/share/bash-completion/completions/wtf.bash-completion +134 -0
- wtftools-0.0.0.dist-info/METADATA +246 -0
- wtftools-0.0.0.dist-info/RECORD +20 -0
- wtftools-0.0.0.dist-info/WHEEL +5 -0
- wtftools-0.0.0.dist-info/entry_points.txt +3 -0
- wtftools-0.0.0.dist-info/licenses/LICENSE +21 -0
- wtftools-0.0.0.dist-info/top_level.txt +1 -0
wtftools/colors.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""ANSI color helpers for wtftools terminal output."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
RESET = "\033[0m"
|
|
11
|
+
BOLD = "\033[1m"
|
|
12
|
+
DIM = "\033[2m"
|
|
13
|
+
|
|
14
|
+
FG_BLACK = "\033[30m"
|
|
15
|
+
FG_RED = "\033[31m"
|
|
16
|
+
FG_GREEN = "\033[32m"
|
|
17
|
+
FG_YELLOW = "\033[33m"
|
|
18
|
+
FG_BLUE = "\033[34m"
|
|
19
|
+
FG_MAGENTA = "\033[35m"
|
|
20
|
+
FG_CYAN = "\033[36m"
|
|
21
|
+
FG_WHITE = "\033[37m"
|
|
22
|
+
FG_BRIGHT_BLACK = "\033[90m"
|
|
23
|
+
|
|
24
|
+
_color_enabled = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def init_colors(force_no_color: bool = False) -> None:
|
|
28
|
+
"""Decide whether colored output should be enabled."""
|
|
29
|
+
global _color_enabled
|
|
30
|
+
if force_no_color:
|
|
31
|
+
_color_enabled = False
|
|
32
|
+
return
|
|
33
|
+
if os.getenv("NO_COLOR"):
|
|
34
|
+
_color_enabled = False
|
|
35
|
+
return
|
|
36
|
+
if not sys.stdout.isatty():
|
|
37
|
+
_color_enabled = False
|
|
38
|
+
return
|
|
39
|
+
_color_enabled = True
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def colored(text: str, color: str, bold: bool = False) -> str:
|
|
43
|
+
"""Wrap text in ANSI color codes when colors are enabled."""
|
|
44
|
+
if not _color_enabled:
|
|
45
|
+
return text
|
|
46
|
+
prefix = (BOLD if bold else "") + color
|
|
47
|
+
return f"{prefix}{text}{RESET}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def green(text: str, bold: bool = False) -> str:
|
|
51
|
+
return colored(text, FG_GREEN, bold=bold)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def red(text: str, bold: bool = False) -> str:
|
|
55
|
+
return colored(text, FG_RED, bold=bold)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def yellow(text: str, bold: bool = False) -> str:
|
|
59
|
+
return colored(text, FG_YELLOW, bold=bold)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cyan(text: str, bold: bool = False) -> str:
|
|
63
|
+
return colored(text, FG_CYAN, bold=bold)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def blue(text: str, bold: bool = False) -> str:
|
|
67
|
+
return colored(text, FG_BLUE, bold=bold)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def dim(text: str) -> str:
|
|
71
|
+
if not _color_enabled:
|
|
72
|
+
return text
|
|
73
|
+
return f"{DIM}{text}{RESET}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def bold(text: str) -> str:
|
|
77
|
+
if not _color_enabled:
|
|
78
|
+
return text
|
|
79
|
+
return f"{BOLD}{text}{RESET}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def status_marker(status: str) -> str:
|
|
83
|
+
"""Render a status marker like [OK] / [WARN] / [FAIL]."""
|
|
84
|
+
s = status.upper()
|
|
85
|
+
if s == "OK":
|
|
86
|
+
return green("[ OK ]", bold=True)
|
|
87
|
+
if s in ("WARN", "WARNING"):
|
|
88
|
+
return yellow("[WARN]", bold=True)
|
|
89
|
+
if s in ("FAIL", "ERROR", "CRIT", "CRITICAL"):
|
|
90
|
+
return red("[FAIL]", bold=True)
|
|
91
|
+
if s in ("INFO", "SKIP", "N/A"):
|
|
92
|
+
return cyan(f"[{s:^4}]")
|
|
93
|
+
return f"[{s}]"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def section(title: str, width: Optional[int] = None) -> str:
|
|
97
|
+
"""Render a section header."""
|
|
98
|
+
if width is None:
|
|
99
|
+
try:
|
|
100
|
+
width = shutil.get_terminal_size((80, 24)).columns
|
|
101
|
+
except OSError:
|
|
102
|
+
width = 80
|
|
103
|
+
title = f" {title.strip()} "
|
|
104
|
+
if len(title) >= width:
|
|
105
|
+
return bold(title)
|
|
106
|
+
side = (width - len(title)) // 2
|
|
107
|
+
bar = "─" * side
|
|
108
|
+
line = f"{bar}{title}{bar}"
|
|
109
|
+
if len(line) < width:
|
|
110
|
+
line += "─"
|
|
111
|
+
return cyan(line, bold=True)
|
wtftools/config.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Configuration loading for wtftools.
|
|
4
|
+
|
|
5
|
+
Reads INI files (no extra deps; stdlib `configparser`). Discovery order:
|
|
6
|
+
/etc/wtftools/config.ini
|
|
7
|
+
/etc/wtf/config.ini
|
|
8
|
+
$XDG_CONFIG_HOME/wtftools/config.ini (or ~/.config/wtftools/config.ini)
|
|
9
|
+
|
|
10
|
+
Later files override earlier ones. Example:
|
|
11
|
+
|
|
12
|
+
[thresholds]
|
|
13
|
+
disk_warn = 85
|
|
14
|
+
disk_fail = 95
|
|
15
|
+
swap_warn = 50
|
|
16
|
+
swap_fail = 90
|
|
17
|
+
|
|
18
|
+
[ignore]
|
|
19
|
+
checks = swap, updates
|
|
20
|
+
# disk results carry a result-name like "disk /mnt/Backup" — you can
|
|
21
|
+
# ignore individual mountpoints by listing them in `result_names`.
|
|
22
|
+
result_names =
|
|
23
|
+
disk /mnt/Backup
|
|
24
|
+
disk /mnt/Video
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import configparser
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
import traceback
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import List, Optional, Set
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
DEFAULT_CONFIG_PATHS = (
|
|
37
|
+
"/etc/wtftools/config.ini",
|
|
38
|
+
"/etc/wtf/config.ini",
|
|
39
|
+
os.path.join(
|
|
40
|
+
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
|
|
41
|
+
"wtftools",
|
|
42
|
+
"config.ini",
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Config:
|
|
49
|
+
"""All runtime knobs in one place. Defaults match the hardcoded values."""
|
|
50
|
+
|
|
51
|
+
disk_warn_pct: int = 85
|
|
52
|
+
disk_fail_pct: int = 95
|
|
53
|
+
mem_warn_pct: int = 85
|
|
54
|
+
mem_fail_pct: int = 95
|
|
55
|
+
swap_warn_pct: int = 30
|
|
56
|
+
swap_fail_pct: int = 70
|
|
57
|
+
load_warn_ratio: float = 1.0
|
|
58
|
+
load_fail_ratio: float = 2.0
|
|
59
|
+
iowait_warn_pct: float = 10.0
|
|
60
|
+
iowait_fail_pct: float = 30.0
|
|
61
|
+
fd_warn_pct: int = 60
|
|
62
|
+
fd_fail_pct: int = 80
|
|
63
|
+
pid_warn_pct: int = 50
|
|
64
|
+
pid_fail_pct: int = 80
|
|
65
|
+
tcp_retrans_warn_pct: float = 1.0
|
|
66
|
+
tcp_retrans_fail_pct: float = 5.0
|
|
67
|
+
auth_warn_count: int = 50
|
|
68
|
+
restart_warn_threshold: int = 3
|
|
69
|
+
restart_fail_threshold: int = 10
|
|
70
|
+
psi_warn_pct: float = 10.0
|
|
71
|
+
psi_fail_pct: float = 30.0
|
|
72
|
+
cert_warn_days: int = 30
|
|
73
|
+
cert_fail_days: int = 7
|
|
74
|
+
conntrack_warn_pct: int = 70
|
|
75
|
+
conntrack_fail_pct: int = 90
|
|
76
|
+
journal_warn_gb: float = 4.0
|
|
77
|
+
journal_fail_gb: float = 16.0
|
|
78
|
+
temp_warn_c: float = 75.0
|
|
79
|
+
temp_fail_c: float = 90.0
|
|
80
|
+
dns_probe_hosts: str = "google.com,cloudflare.com"
|
|
81
|
+
dns_probe_timeout: float = 2.0
|
|
82
|
+
http_probes: str = ""
|
|
83
|
+
tcp_probes: str = ""
|
|
84
|
+
probe_timeout: float = 3.0
|
|
85
|
+
probe_slow_ms: float = 1000.0
|
|
86
|
+
fleet_hosts: str = ""
|
|
87
|
+
check_timeout_seconds: float = 10.0
|
|
88
|
+
parallel_workers: int = 8
|
|
89
|
+
ignored_checks: Set[str] = field(default_factory=set)
|
|
90
|
+
ignored_result_names: Set[str] = field(default_factory=set)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_CURRENT: Config = Config()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_config() -> Config:
|
|
97
|
+
"""Return the active Config singleton."""
|
|
98
|
+
return _CURRENT
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def set_config(cfg: Config) -> None:
|
|
102
|
+
"""Replace the active Config (called from CLI / tests)."""
|
|
103
|
+
global _CURRENT
|
|
104
|
+
_CURRENT = cfg
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _split_list(value: str) -> List[str]:
|
|
108
|
+
return [s.strip() for s in value.replace("\n", ",").split(",") if s.strip()]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _coerce(parser: configparser.ConfigParser, section: str, option: str, current_value, cfg_attr: str, cfg: Config) -> None:
|
|
112
|
+
"""Read one config option and assign with the right numeric type."""
|
|
113
|
+
if not parser.has_option(section, option):
|
|
114
|
+
return
|
|
115
|
+
raw = parser.get(section, option)
|
|
116
|
+
try:
|
|
117
|
+
if isinstance(current_value, bool):
|
|
118
|
+
new_value = raw.strip().lower() in ("1", "true", "yes", "on")
|
|
119
|
+
elif isinstance(current_value, int):
|
|
120
|
+
new_value = int(float(raw))
|
|
121
|
+
elif isinstance(current_value, float):
|
|
122
|
+
new_value = float(raw)
|
|
123
|
+
else:
|
|
124
|
+
new_value = raw
|
|
125
|
+
setattr(cfg, cfg_attr, new_value)
|
|
126
|
+
except (ValueError, TypeError):
|
|
127
|
+
logger.warning(f"config: ignoring invalid value for {section}.{option}: {raw!r}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def load_config(paths: Optional[List[str]] = None) -> Config:
|
|
131
|
+
"""Read config files and return a Config with defaults overlaid by user values."""
|
|
132
|
+
if paths is None:
|
|
133
|
+
paths = list(DEFAULT_CONFIG_PATHS)
|
|
134
|
+
cfg = Config()
|
|
135
|
+
existing = [p for p in paths if os.path.exists(p)]
|
|
136
|
+
if not existing:
|
|
137
|
+
return cfg
|
|
138
|
+
parser = configparser.ConfigParser(interpolation=None)
|
|
139
|
+
try:
|
|
140
|
+
parser.read(existing, encoding="utf-8")
|
|
141
|
+
except (configparser.Error, OSError) as exc:
|
|
142
|
+
logger.warning(f"config: cannot read {existing}: {type(exc).__name__}: {exc}\n" f"{traceback.format_exc()}")
|
|
143
|
+
return cfg
|
|
144
|
+
|
|
145
|
+
threshold_map = [
|
|
146
|
+
("disk_warn", "disk_warn_pct"),
|
|
147
|
+
("disk_fail", "disk_fail_pct"),
|
|
148
|
+
("mem_warn", "mem_warn_pct"),
|
|
149
|
+
("mem_fail", "mem_fail_pct"),
|
|
150
|
+
("swap_warn", "swap_warn_pct"),
|
|
151
|
+
("swap_fail", "swap_fail_pct"),
|
|
152
|
+
("load_warn", "load_warn_ratio"),
|
|
153
|
+
("load_fail", "load_fail_ratio"),
|
|
154
|
+
("iowait_warn", "iowait_warn_pct"),
|
|
155
|
+
("iowait_fail", "iowait_fail_pct"),
|
|
156
|
+
("fd_warn", "fd_warn_pct"),
|
|
157
|
+
("fd_fail", "fd_fail_pct"),
|
|
158
|
+
("pid_warn", "pid_warn_pct"),
|
|
159
|
+
("pid_fail", "pid_fail_pct"),
|
|
160
|
+
("tcp_retrans_warn", "tcp_retrans_warn_pct"),
|
|
161
|
+
("tcp_retrans_fail", "tcp_retrans_fail_pct"),
|
|
162
|
+
("auth_warn", "auth_warn_count"),
|
|
163
|
+
("restart_warn", "restart_warn_threshold"),
|
|
164
|
+
("restart_fail", "restart_fail_threshold"),
|
|
165
|
+
("psi_warn", "psi_warn_pct"),
|
|
166
|
+
("psi_fail", "psi_fail_pct"),
|
|
167
|
+
("cert_warn_days", "cert_warn_days"),
|
|
168
|
+
("cert_fail_days", "cert_fail_days"),
|
|
169
|
+
("conntrack_warn", "conntrack_warn_pct"),
|
|
170
|
+
("conntrack_fail", "conntrack_fail_pct"),
|
|
171
|
+
("journal_warn_gb", "journal_warn_gb"),
|
|
172
|
+
("journal_fail_gb", "journal_fail_gb"),
|
|
173
|
+
("temp_warn_c", "temp_warn_c"),
|
|
174
|
+
("temp_fail_c", "temp_fail_c"),
|
|
175
|
+
("dns_probe_hosts", "dns_probe_hosts"),
|
|
176
|
+
("dns_probe_timeout", "dns_probe_timeout"),
|
|
177
|
+
("http_probes", "http_probes"),
|
|
178
|
+
("tcp_probes", "tcp_probes"),
|
|
179
|
+
("probe_timeout", "probe_timeout"),
|
|
180
|
+
("probe_slow_ms", "probe_slow_ms"),
|
|
181
|
+
("fleet_hosts", "fleet_hosts"),
|
|
182
|
+
("check_timeout", "check_timeout_seconds"),
|
|
183
|
+
("parallel_workers", "parallel_workers"),
|
|
184
|
+
]
|
|
185
|
+
if parser.has_section("thresholds"):
|
|
186
|
+
for opt, attr in threshold_map:
|
|
187
|
+
_coerce(parser, "thresholds", opt, getattr(cfg, attr), attr, cfg)
|
|
188
|
+
|
|
189
|
+
if parser.has_section("ignore"):
|
|
190
|
+
if parser.has_option("ignore", "checks"):
|
|
191
|
+
cfg.ignored_checks = set(_split_list(parser.get("ignore", "checks")))
|
|
192
|
+
if parser.has_option("ignore", "result_names"):
|
|
193
|
+
cfg.ignored_result_names = set(_split_list(parser.get("ignore", "result_names")))
|
|
194
|
+
return cfg
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def example_config() -> str:
|
|
198
|
+
"""Return an example config file body — used by `wtf config --example`."""
|
|
199
|
+
return """# wtftools example config — drop at /etc/wtftools/config.ini
|
|
200
|
+
# or ~/.config/wtftools/config.ini
|
|
201
|
+
|
|
202
|
+
[thresholds]
|
|
203
|
+
# Disk usage % thresholds (warn at >= warn, fail at >= fail).
|
|
204
|
+
disk_warn = 85
|
|
205
|
+
disk_fail = 95
|
|
206
|
+
|
|
207
|
+
# Memory %
|
|
208
|
+
mem_warn = 85
|
|
209
|
+
mem_fail = 95
|
|
210
|
+
|
|
211
|
+
# Swap % (a server that uses swap heavily is doing something wrong)
|
|
212
|
+
swap_warn = 30
|
|
213
|
+
swap_fail = 70
|
|
214
|
+
|
|
215
|
+
# Load avg ratio relative to CPU count
|
|
216
|
+
load_warn = 1.0
|
|
217
|
+
load_fail = 2.0
|
|
218
|
+
|
|
219
|
+
# CPU iowait %
|
|
220
|
+
iowait_warn = 10
|
|
221
|
+
iowait_fail = 30
|
|
222
|
+
|
|
223
|
+
# Open file descriptors % of fs.file-max
|
|
224
|
+
fd_warn = 60
|
|
225
|
+
fd_fail = 80
|
|
226
|
+
|
|
227
|
+
# Process count % of pid_max
|
|
228
|
+
pid_warn = 50
|
|
229
|
+
pid_fail = 80
|
|
230
|
+
|
|
231
|
+
# TCP retransmit % (sampled over 1 second)
|
|
232
|
+
tcp_retrans_warn = 1.0
|
|
233
|
+
tcp_retrans_fail = 5.0
|
|
234
|
+
|
|
235
|
+
# Failed auth attempts in the look-back window
|
|
236
|
+
auth_warn = 50
|
|
237
|
+
|
|
238
|
+
# Service NRestarts thresholds
|
|
239
|
+
restart_warn = 3
|
|
240
|
+
restart_fail = 10
|
|
241
|
+
|
|
242
|
+
[ignore]
|
|
243
|
+
# Skip these check short-names entirely (comma- or newline-separated).
|
|
244
|
+
# Run `wtf audit --list-checks` to see all names.
|
|
245
|
+
checks =
|
|
246
|
+
|
|
247
|
+
# Skip specific result names (useful for disks: "disk /mnt/Backup")
|
|
248
|
+
result_names =
|
|
249
|
+
"""
|