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/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
+ """