tunectl 1.0.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/LICENSE +21 -0
- package/README.md +303 -0
- package/bin/tunectl +43 -0
- package/package.json +33 -0
- package/scripts/audit.sh +693 -0
- package/scripts/benchmark.sh +623 -0
- package/scripts/discover.sh +367 -0
- package/scripts/rollback.sh +267 -0
- package/scripts/tune.sh +1073 -0
- package/setup.py +5 -0
- package/tune-manifest.json +993 -0
- package/tunectl/__init__.py +3 -0
- package/tunectl/__main__.py +6 -0
- package/tunectl/cli.py +433 -0
package/scripts/tune.sh
ADDED
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# tune.sh — Main tuning engine for tunectl
|
|
5
|
+
# Reads tune-manifest.json, applies tuning based on tier selection.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# tune.sh --tier <conservative|balanced|aggressive> [--dry-run|--apply]
|
|
9
|
+
#
|
|
10
|
+
# Exit codes: 0=success, 1=failure, 2=usage error
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
MANIFEST="${SCRIPT_DIR}/../tune-manifest.json"
|
|
14
|
+
BACKUP_ROOT="/var/lib/tunectl/backups"
|
|
15
|
+
|
|
16
|
+
# -------------------------------------------------------
|
|
17
|
+
# Usage / help
|
|
18
|
+
# -------------------------------------------------------
|
|
19
|
+
usage() {
|
|
20
|
+
cat >&2 <<EOF
|
|
21
|
+
Usage: tune.sh --tier <tier> [--dry-run|--apply]
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--tier <tier> Required. One of: conservative, balanced, aggressive
|
|
25
|
+
--dry-run Show plan without making changes (default)
|
|
26
|
+
--apply Apply tuning changes (requires root)
|
|
27
|
+
--help Show this help message
|
|
28
|
+
|
|
29
|
+
Tiers:
|
|
30
|
+
conservative risk=none entries only (51 entries)
|
|
31
|
+
balanced risk=none + risk=low entries (80 entries)
|
|
32
|
+
aggressive all entries including risk=med (86 entries)
|
|
33
|
+
|
|
34
|
+
Exit codes:
|
|
35
|
+
0 Success
|
|
36
|
+
1 Operational failure
|
|
37
|
+
2 Usage error
|
|
38
|
+
EOF
|
|
39
|
+
exit 2
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# -------------------------------------------------------
|
|
43
|
+
# Globals
|
|
44
|
+
# -------------------------------------------------------
|
|
45
|
+
TIER=""
|
|
46
|
+
MODE="dry-run"
|
|
47
|
+
|
|
48
|
+
# -------------------------------------------------------
|
|
49
|
+
# Parse arguments
|
|
50
|
+
# -------------------------------------------------------
|
|
51
|
+
parse_args() {
|
|
52
|
+
while [[ $# -gt 0 ]]; do
|
|
53
|
+
case "$1" in
|
|
54
|
+
--tier)
|
|
55
|
+
[[ $# -lt 2 ]] && { echo "Error: --tier requires a value" >&2; usage; }
|
|
56
|
+
TIER="$2"
|
|
57
|
+
shift 2
|
|
58
|
+
;;
|
|
59
|
+
--dry-run)
|
|
60
|
+
MODE="dry-run"
|
|
61
|
+
shift
|
|
62
|
+
;;
|
|
63
|
+
--apply)
|
|
64
|
+
MODE="apply"
|
|
65
|
+
shift
|
|
66
|
+
;;
|
|
67
|
+
--help|-h)
|
|
68
|
+
usage
|
|
69
|
+
;;
|
|
70
|
+
*)
|
|
71
|
+
echo "Error: Unknown argument: $1" >&2
|
|
72
|
+
usage
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
# Require --tier
|
|
78
|
+
if [[ -z "$TIER" ]]; then
|
|
79
|
+
echo "Error: --tier is required" >&2
|
|
80
|
+
usage
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Validate tier
|
|
84
|
+
case "$TIER" in
|
|
85
|
+
conservative|balanced|aggressive) ;;
|
|
86
|
+
*)
|
|
87
|
+
echo "Error: Invalid tier '$TIER'. Valid tiers: conservative, balanced, aggressive" >&2
|
|
88
|
+
exit 2
|
|
89
|
+
;;
|
|
90
|
+
esac
|
|
91
|
+
|
|
92
|
+
# Apply mode requires root
|
|
93
|
+
if [[ "$MODE" == "apply" && $EUID -ne 0 ]]; then
|
|
94
|
+
echo "Error: --apply requires root privileges. Run with sudo." >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# -------------------------------------------------------
|
|
100
|
+
# Validate manifest
|
|
101
|
+
# -------------------------------------------------------
|
|
102
|
+
validate_manifest() {
|
|
103
|
+
if [[ ! -f "$MANIFEST" ]]; then
|
|
104
|
+
echo "Error: Manifest not found at $MANIFEST" >&2
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
if ! jq empty "$MANIFEST" 2>/dev/null; then
|
|
109
|
+
echo "Error: Manifest is not valid JSON: $MANIFEST" >&2
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# -------------------------------------------------------
|
|
115
|
+
# Detect system RAM in KB
|
|
116
|
+
# -------------------------------------------------------
|
|
117
|
+
detect_ram_kb() {
|
|
118
|
+
awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo "0"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# -------------------------------------------------------
|
|
122
|
+
# Get tier jq filter expression
|
|
123
|
+
# -------------------------------------------------------
|
|
124
|
+
get_tier_filter() {
|
|
125
|
+
case "$TIER" in
|
|
126
|
+
conservative) echo 'select(.risk == "none")' ;;
|
|
127
|
+
balanced) echo 'select(.risk == "none" or .risk == "low")' ;;
|
|
128
|
+
aggressive) echo '.' ;;
|
|
129
|
+
esac
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# -------------------------------------------------------
|
|
133
|
+
# Round up to the nearest power of 2 (for memory sizes)
|
|
134
|
+
# -------------------------------------------------------
|
|
135
|
+
round_to_power_of_2() {
|
|
136
|
+
local val="$1"
|
|
137
|
+
awk "BEGIN {
|
|
138
|
+
v = $val
|
|
139
|
+
if (v <= 0) { print 1; exit }
|
|
140
|
+
p = 1
|
|
141
|
+
while (p < v) p *= 2
|
|
142
|
+
# Pick the nearest power of 2 (up or down)
|
|
143
|
+
lower = p / 2
|
|
144
|
+
if ((v - lower) <= (p - v)) print lower
|
|
145
|
+
else print p
|
|
146
|
+
}"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# -------------------------------------------------------
|
|
150
|
+
# Scale a numeric value proportionally based on RAM
|
|
151
|
+
# Reference: 8GB
|
|
152
|
+
# Memory sizes are rounded to the nearest power of 2.
|
|
153
|
+
# -------------------------------------------------------
|
|
154
|
+
scale_value() {
|
|
155
|
+
local entry_id="$1"
|
|
156
|
+
local raw_value="$2"
|
|
157
|
+
local ram_kb="$3"
|
|
158
|
+
local ram_gb
|
|
159
|
+
ram_gb=$(awk "BEGIN {printf \"%.2f\", $ram_kb / 1048576}")
|
|
160
|
+
|
|
161
|
+
local ref_ram_gb=8
|
|
162
|
+
|
|
163
|
+
case "$entry_id" in
|
|
164
|
+
MEM-006)
|
|
165
|
+
local scaled
|
|
166
|
+
scaled=$(awk "BEGIN {v = 131072 * ($ram_gb / $ref_ram_gb); printf \"%.0f\", v}")
|
|
167
|
+
scaled=$(round_to_power_of_2 "$scaled")
|
|
168
|
+
[[ $scaled -lt 65536 ]] && scaled=65536
|
|
169
|
+
echo "$scaled"
|
|
170
|
+
;;
|
|
171
|
+
SWAP-007)
|
|
172
|
+
local size_gb
|
|
173
|
+
size_gb=$(awk "BEGIN {v = $ram_gb * 1.5; printf \"%.0f\", v}")
|
|
174
|
+
[[ $size_gb -lt 1 ]] && size_gb=1
|
|
175
|
+
echo "${size_gb}G"
|
|
176
|
+
;;
|
|
177
|
+
FS-003)
|
|
178
|
+
local size_gb
|
|
179
|
+
size_gb=$(awk "BEGIN {v = $ram_gb * 0.5; printf \"%.0f\", v}")
|
|
180
|
+
[[ $size_gb -lt 1 ]] && size_gb=1
|
|
181
|
+
echo "tmpfs /tmp tmpfs defaults,noatime,size=${size_gb}G 0 0"
|
|
182
|
+
;;
|
|
183
|
+
FS-006)
|
|
184
|
+
local scaled
|
|
185
|
+
scaled=$(awk "BEGIN {v = 524288 * ($ram_gb / $ref_ram_gb); printf \"%.0f\", v}")
|
|
186
|
+
scaled=$(round_to_power_of_2 "$scaled")
|
|
187
|
+
[[ $scaled -lt 65536 ]] && scaled=65536
|
|
188
|
+
echo "$scaled"
|
|
189
|
+
;;
|
|
190
|
+
CGRP-002)
|
|
191
|
+
local size_gb
|
|
192
|
+
size_gb=$(awk "BEGIN {v = $ram_gb * 0.6; printf \"%.0f\", v}")
|
|
193
|
+
[[ $size_gb -lt 1 ]] && size_gb=1
|
|
194
|
+
echo "${size_gb}G"
|
|
195
|
+
;;
|
|
196
|
+
CGRP-008)
|
|
197
|
+
local size_gb
|
|
198
|
+
size_gb=$(awk "BEGIN {v = $ram_gb * 0.35; printf \"%.0f\", v}")
|
|
199
|
+
[[ $size_gb -lt 1 ]] && size_gb=1
|
|
200
|
+
echo "${size_gb}G"
|
|
201
|
+
;;
|
|
202
|
+
*)
|
|
203
|
+
echo "$raw_value"
|
|
204
|
+
;;
|
|
205
|
+
esac
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# -------------------------------------------------------
|
|
209
|
+
# Check if jemalloc library exists
|
|
210
|
+
# -------------------------------------------------------
|
|
211
|
+
check_jemalloc() {
|
|
212
|
+
[[ -f "/usr/lib/x86_64-linux-gnu/libjemalloc.so.2" ]] && return 0
|
|
213
|
+
ldconfig -p 2>/dev/null | grep -q libjemalloc && return 0
|
|
214
|
+
return 1
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# -------------------------------------------------------
|
|
218
|
+
# Check if a systemd service unit file exists
|
|
219
|
+
# -------------------------------------------------------
|
|
220
|
+
service_exists() {
|
|
221
|
+
local svc="$1"
|
|
222
|
+
systemctl list-unit-files "$svc" 2>/dev/null | grep -q "$svc"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# -------------------------------------------------------
|
|
226
|
+
# Get current value for a sysctl parameter
|
|
227
|
+
# -------------------------------------------------------
|
|
228
|
+
get_sysctl_value() {
|
|
229
|
+
sysctl -n "$1" 2>/dev/null || echo "not set"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# -------------------------------------------------------
|
|
233
|
+
# DRY-RUN: Display the tuning plan
|
|
234
|
+
# -------------------------------------------------------
|
|
235
|
+
do_dry_run() {
|
|
236
|
+
local ram_kb
|
|
237
|
+
ram_kb=$(detect_ram_kb)
|
|
238
|
+
local ram_mb=$((ram_kb / 1024))
|
|
239
|
+
local ram_gb
|
|
240
|
+
ram_gb=$(awk "BEGIN {printf \"%.1f\", $ram_kb / 1048576}")
|
|
241
|
+
|
|
242
|
+
local filter
|
|
243
|
+
filter=$(get_tier_filter)
|
|
244
|
+
|
|
245
|
+
echo "============================================"
|
|
246
|
+
echo " tunectl tune plan — Tier: $TIER (DRY RUN)"
|
|
247
|
+
echo "============================================"
|
|
248
|
+
echo ""
|
|
249
|
+
echo "System RAM: ${ram_mb} MB (${ram_gb} GB)"
|
|
250
|
+
echo ""
|
|
251
|
+
|
|
252
|
+
# Extract all filtered entries as tab-separated lines in a SINGLE jq call
|
|
253
|
+
local tsv_data
|
|
254
|
+
tsv_data=$(jq -r "[.tuning_entries[] | $filter] | .[] | [.id, .category, .parameter, .config_file, .before_value, .after_value, .risk, (.requires_reboot | tostring), (.scaling_note // \"\")] | @tsv" "$MANIFEST")
|
|
255
|
+
|
|
256
|
+
local count
|
|
257
|
+
count=$(echo "$tsv_data" | wc -l)
|
|
258
|
+
|
|
259
|
+
echo "Entries to apply: $count"
|
|
260
|
+
echo ""
|
|
261
|
+
|
|
262
|
+
# Category summary from the same data
|
|
263
|
+
echo "--- Category Summary ---"
|
|
264
|
+
echo "$tsv_data" | awk -F'\t' '{print $2}' | sort | uniq -c | sort -rn | while read -r cnt cat; do
|
|
265
|
+
echo " $cat: $cnt"
|
|
266
|
+
done
|
|
267
|
+
echo ""
|
|
268
|
+
|
|
269
|
+
# Reboot required count
|
|
270
|
+
local reboot_count
|
|
271
|
+
reboot_count=$(echo "$tsv_data" | awk -F'\t' '$8 == "true"' | wc -l)
|
|
272
|
+
if [[ $reboot_count -gt 0 ]]; then
|
|
273
|
+
echo "⚠ $reboot_count entries require reboot"
|
|
274
|
+
echo ""
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
echo "--- Tuning Plan ---"
|
|
278
|
+
printf "%-12s %-42s %-40s %-20s → %-20s %s\n" "ID" "Parameter" "Config File" "Current" "Target" "Notes"
|
|
279
|
+
printf '%0.s-' {1..160}
|
|
280
|
+
echo ""
|
|
281
|
+
|
|
282
|
+
# Pre-fetch all sysctl values in one batch for speed
|
|
283
|
+
declare -A sysctl_cache
|
|
284
|
+
local sysctl_params
|
|
285
|
+
sysctl_params=$(echo "$tsv_data" | awk -F'\t' '$4 == "/etc/sysctl.d/99-performance.conf" {print $3}')
|
|
286
|
+
if [[ -n "$sysctl_params" ]]; then
|
|
287
|
+
while IFS= read -r param; do
|
|
288
|
+
sysctl_cache["$param"]=$(sysctl -n "$param" 2>/dev/null || echo "not set")
|
|
289
|
+
done <<< "$sysctl_params"
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
# Pre-check which services exist (batch)
|
|
293
|
+
declare -A svc_exists_cache
|
|
294
|
+
local svc_names
|
|
295
|
+
svc_names=$(echo "$tsv_data" | awk -F'\t' '$4 == "systemctl disable/mask" {print $3}')
|
|
296
|
+
if [[ -n "$svc_names" ]]; then
|
|
297
|
+
# Get all unit files in one call (include both services and sockets)
|
|
298
|
+
local all_units
|
|
299
|
+
all_units=$( (systemctl list-unit-files --type=service --no-legend 2>/dev/null; systemctl list-unit-files --type=socket --no-legend 2>/dev/null) | awk '{print $1}' || echo "")
|
|
300
|
+
while IFS= read -r svc; do
|
|
301
|
+
[[ -z "$svc" ]] && continue
|
|
302
|
+
if echo "$all_units" | grep -qF "$svc"; then
|
|
303
|
+
svc_exists_cache["$svc"]="yes"
|
|
304
|
+
else
|
|
305
|
+
svc_exists_cache["$svc"]="no"
|
|
306
|
+
fi
|
|
307
|
+
done <<< "$svc_names"
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
# Check jemalloc once
|
|
311
|
+
local has_jemalloc=false
|
|
312
|
+
check_jemalloc && has_jemalloc=true
|
|
313
|
+
|
|
314
|
+
# Iterate entries
|
|
315
|
+
while IFS=$'\t' read -r id category parameter config_file before_value after_value risk requires_reboot scaling_note; do
|
|
316
|
+
# Compute target value (with RAM scaling)
|
|
317
|
+
local target_value
|
|
318
|
+
target_value=$(scale_value "$id" "$after_value" "$ram_kb")
|
|
319
|
+
|
|
320
|
+
# Get current value
|
|
321
|
+
local current_value=""
|
|
322
|
+
case "$config_file" in
|
|
323
|
+
/etc/sysctl.d/99-performance.conf)
|
|
324
|
+
current_value="${sysctl_cache[$parameter]:-not set}"
|
|
325
|
+
;;
|
|
326
|
+
"systemctl disable/mask")
|
|
327
|
+
if [[ "${svc_exists_cache[$parameter]:-no}" == "yes" ]]; then
|
|
328
|
+
current_value=$(systemctl is-enabled "$parameter" 2>/dev/null || echo "unknown")
|
|
329
|
+
else
|
|
330
|
+
current_value="not installed"
|
|
331
|
+
fi
|
|
332
|
+
;;
|
|
333
|
+
"systemctl")
|
|
334
|
+
current_value="active"
|
|
335
|
+
;;
|
|
336
|
+
/etc/environment)
|
|
337
|
+
case "$id" in
|
|
338
|
+
MEM-019) current_value=$(grep "^LD_PRELOAD=" /etc/environment 2>/dev/null | cut -d= -f2- || echo "not set") ;;
|
|
339
|
+
BUILD-001) current_value=$(grep "^RUSTC_WRAPPER=" /etc/environment 2>/dev/null | cut -d= -f2- || echo "not set") ;;
|
|
340
|
+
*) current_value="not set" ;;
|
|
341
|
+
esac
|
|
342
|
+
[[ -z "$current_value" ]] && current_value="not set"
|
|
343
|
+
;;
|
|
344
|
+
/etc/default/grub)
|
|
345
|
+
# Extract specific boot parameter from GRUB_CMDLINE_LINUX_DEFAULT
|
|
346
|
+
current_value="not set"
|
|
347
|
+
if [[ -f "$config_file" ]]; then
|
|
348
|
+
local grub_cmdline
|
|
349
|
+
grub_cmdline=$(grep '^GRUB_CMDLINE_LINUX_DEFAULT=' "$config_file" 2>/dev/null | sed 's/^GRUB_CMDLINE_LINUX_DEFAULT=//' | tr -d '"' || echo "")
|
|
350
|
+
local grub_param_name=""
|
|
351
|
+
case "$id" in
|
|
352
|
+
SWAP-009) grub_param_name="zswap.enabled" ;;
|
|
353
|
+
BOOT-001) grub_param_name="mitigations" ;;
|
|
354
|
+
BOOT-002) grub_param_name="l1tf" ;;
|
|
355
|
+
BOOT-003) grub_param_name="tsx_async_abort" ;;
|
|
356
|
+
BOOT-004) grub_param_name="preempt" ;;
|
|
357
|
+
BOOT-005) grub_param_name="transparent_hugepage" ;;
|
|
358
|
+
esac
|
|
359
|
+
if [[ -n "$grub_param_name" && -n "$grub_cmdline" ]]; then
|
|
360
|
+
local grub_val
|
|
361
|
+
grub_val=$(echo "$grub_cmdline" | grep -oE "${grub_param_name}=[^ ]+" | head -1 | cut -d= -f2-)
|
|
362
|
+
if [[ -n "$grub_val" ]]; then
|
|
363
|
+
current_value="$grub_val"
|
|
364
|
+
fi
|
|
365
|
+
fi
|
|
366
|
+
fi
|
|
367
|
+
;;
|
|
368
|
+
/etc/fstab)
|
|
369
|
+
# Extract fstab entry details
|
|
370
|
+
current_value="not configured"
|
|
371
|
+
if [[ -f "$config_file" ]]; then
|
|
372
|
+
case "$id" in
|
|
373
|
+
SWAP-005)
|
|
374
|
+
if grep -q '/dev/zram0' "$config_file" 2>/dev/null; then
|
|
375
|
+
current_value=$(grep '/dev/zram0' "$config_file" | head -1 | sed 's/\s\+/ /g')
|
|
376
|
+
fi
|
|
377
|
+
;;
|
|
378
|
+
FS-001)
|
|
379
|
+
local root_opts
|
|
380
|
+
root_opts=$(awk '$2=="/" {print $4}' "$config_file" 2>/dev/null)
|
|
381
|
+
if [[ -n "$root_opts" ]]; then
|
|
382
|
+
if echo "$root_opts" | grep -q 'noatime'; then
|
|
383
|
+
current_value="noatime"
|
|
384
|
+
elif echo "$root_opts" | grep -q 'relatime'; then
|
|
385
|
+
current_value="relatime"
|
|
386
|
+
else
|
|
387
|
+
current_value="defaults"
|
|
388
|
+
fi
|
|
389
|
+
fi
|
|
390
|
+
;;
|
|
391
|
+
FS-002)
|
|
392
|
+
local root_opts
|
|
393
|
+
root_opts=$(awk '$2=="/" {print $4}' "$config_file" 2>/dev/null)
|
|
394
|
+
if [[ -n "$root_opts" ]]; then
|
|
395
|
+
local commit_val
|
|
396
|
+
commit_val=$(echo "$root_opts" | grep -oE 'commit=[0-9]+' | cut -d= -f2)
|
|
397
|
+
if [[ -n "$commit_val" ]]; then
|
|
398
|
+
current_value="${commit_val} (seconds)"
|
|
399
|
+
else
|
|
400
|
+
current_value="5 (default)"
|
|
401
|
+
fi
|
|
402
|
+
fi
|
|
403
|
+
;;
|
|
404
|
+
FS-003)
|
|
405
|
+
if grep -q 'tmpfs.*\/tmp' "$config_file" 2>/dev/null; then
|
|
406
|
+
current_value=$(grep 'tmpfs.*\/tmp' "$config_file" | head -1 | sed 's/\s\+/ /g')
|
|
407
|
+
fi
|
|
408
|
+
;;
|
|
409
|
+
esac
|
|
410
|
+
fi
|
|
411
|
+
;;
|
|
412
|
+
/etc/tmpfiles.d/thp.conf|/etc/tmpfiles.d/ksm.conf)
|
|
413
|
+
# Read live values from /sys/kernel/mm/
|
|
414
|
+
current_value="not set"
|
|
415
|
+
case "$id" in
|
|
416
|
+
MEM-012) current_value=$(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null | grep -oE '\[([a-z]+)\]' | tr -d '[]' || echo "not set") ;;
|
|
417
|
+
MEM-013) current_value=$(cat /sys/kernel/mm/transparent_hugepage/defrag 2>/dev/null | grep -oE '\[([a-z+]+)\]' | tr -d '[]' || echo "not set") ;;
|
|
418
|
+
MEM-014) current_value=$(cat /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs 2>/dev/null || echo "not set") ;;
|
|
419
|
+
MEM-015) current_value=$(cat /sys/kernel/mm/ksm/run 2>/dev/null || echo "not set") ;;
|
|
420
|
+
MEM-016) current_value=$(cat /sys/kernel/mm/ksm/pages_to_scan 2>/dev/null || echo "not set") ;;
|
|
421
|
+
MEM-017) current_value=$(cat /sys/kernel/mm/ksm/sleep_millisecs 2>/dev/null || echo "not set") ;;
|
|
422
|
+
MEM-018) current_value=$(cat /sys/kernel/mm/ksm/use_zero_pages 2>/dev/null || echo "not set") ;;
|
|
423
|
+
esac
|
|
424
|
+
;;
|
|
425
|
+
/etc/systemd/system/*.slice)
|
|
426
|
+
# Read from systemctl show or parse unit file
|
|
427
|
+
current_value="not present"
|
|
428
|
+
local slice_name
|
|
429
|
+
slice_name=$(basename "$config_file")
|
|
430
|
+
local prop_name=""
|
|
431
|
+
case "$parameter" in
|
|
432
|
+
*CPUWeight*) prop_name="CPUWeight" ;;
|
|
433
|
+
*MemoryHigh*) prop_name="MemoryHigh" ;;
|
|
434
|
+
*IOWeight*) prop_name="IOWeight" ;;
|
|
435
|
+
*ManagedOOMSwap) prop_name="ManagedOOMSwap" ;;
|
|
436
|
+
*ManagedOOMMemoryPressureLimit) prop_name="ManagedOOMMemoryPressureLimit" ;;
|
|
437
|
+
*ManagedOOMMemoryPressure) prop_name="ManagedOOMMemoryPressure" ;;
|
|
438
|
+
esac
|
|
439
|
+
if [[ -n "$prop_name" ]]; then
|
|
440
|
+
local prop_val
|
|
441
|
+
prop_val=$(systemctl show "$slice_name" -p "$prop_name" 2>/dev/null | cut -d= -f2-)
|
|
442
|
+
if [[ -n "$prop_val" && "$prop_val" != "infinity" && "$prop_val" != "[not set]" ]]; then
|
|
443
|
+
# Convert bytes to human-readable for MemoryHigh
|
|
444
|
+
if [[ "$prop_name" == "MemoryHigh" && "$prop_val" =~ ^[0-9]+$ ]]; then
|
|
445
|
+
local gb_val
|
|
446
|
+
gb_val=$(awk "BEGIN {printf \"%.0f\", $prop_val / 1073741824}")
|
|
447
|
+
current_value="${gb_val}G"
|
|
448
|
+
# Convert ManagedOOMMemoryPressureLimit from raw number to percentage
|
|
449
|
+
elif [[ "$prop_name" == "ManagedOOMMemoryPressureLimit" && "$prop_val" =~ ^[0-9]+$ ]]; then
|
|
450
|
+
# systemd reports as basis points (0-10000) or raw. Convert to %
|
|
451
|
+
local pct
|
|
452
|
+
pct=$(awk "BEGIN {v = $prop_val * 100 / 4294967295; printf \"%.0f\", v}")
|
|
453
|
+
current_value="${pct}%"
|
|
454
|
+
else
|
|
455
|
+
current_value="$prop_val"
|
|
456
|
+
fi
|
|
457
|
+
fi
|
|
458
|
+
fi
|
|
459
|
+
;;
|
|
460
|
+
/etc/udev/rules.d/*)
|
|
461
|
+
# Read live values from /sys/block/ for I/O and zram
|
|
462
|
+
current_value="not set"
|
|
463
|
+
case "$id" in
|
|
464
|
+
FS-004)
|
|
465
|
+
local sched
|
|
466
|
+
sched=$(cat /sys/block/vda/queue/scheduler 2>/dev/null || cat /sys/block/sda/queue/scheduler 2>/dev/null || echo "")
|
|
467
|
+
if [[ -n "$sched" ]]; then
|
|
468
|
+
current_value=$(echo "$sched" | grep -oE '\[[a-z_-]+\]' | tr -d '[]' || echo "unknown")
|
|
469
|
+
fi
|
|
470
|
+
;;
|
|
471
|
+
FS-005)
|
|
472
|
+
current_value=$(cat /sys/block/vda/queue/read_ahead_kb 2>/dev/null || cat /sys/block/sda/queue/read_ahead_kb 2>/dev/null || echo "not set")
|
|
473
|
+
;;
|
|
474
|
+
SWAP-006)
|
|
475
|
+
local algo
|
|
476
|
+
algo=$(cat /sys/block/zram0/comp_algorithm 2>/dev/null || echo "")
|
|
477
|
+
if [[ -n "$algo" ]]; then
|
|
478
|
+
current_value=$(echo "$algo" | grep -oE '\[[a-z0-9_-]+\]' | tr -d '[]' || echo "not configured")
|
|
479
|
+
fi
|
|
480
|
+
;;
|
|
481
|
+
SWAP-007)
|
|
482
|
+
local disksize_bytes
|
|
483
|
+
disksize_bytes=$(cat /sys/block/zram0/disksize 2>/dev/null || echo "0")
|
|
484
|
+
if [[ "$disksize_bytes" -gt 0 ]] 2>/dev/null; then
|
|
485
|
+
local disksize_gb
|
|
486
|
+
disksize_gb=$(awk "BEGIN {printf \"%.0f\", $disksize_bytes / 1073741824}")
|
|
487
|
+
current_value="${disksize_gb}G"
|
|
488
|
+
fi
|
|
489
|
+
;;
|
|
490
|
+
esac
|
|
491
|
+
;;
|
|
492
|
+
/etc/modules-load.d/*)
|
|
493
|
+
# Check if module is loaded
|
|
494
|
+
current_value="not loaded"
|
|
495
|
+
local mod_name="$after_value"
|
|
496
|
+
if lsmod 2>/dev/null | grep -q "^${mod_name} "; then
|
|
497
|
+
current_value="loaded"
|
|
498
|
+
fi
|
|
499
|
+
;;
|
|
500
|
+
/usr/local/bin/run-in-*|/usr/local/bin/*)
|
|
501
|
+
# Extract OOMScoreAdjust from helper script
|
|
502
|
+
current_value="not present"
|
|
503
|
+
if [[ -f "$config_file" ]]; then
|
|
504
|
+
local oom_val
|
|
505
|
+
oom_val=$(grep -oE 'OOMScoreAdjust=-?[0-9]+' "$config_file" 2>/dev/null | head -1 | cut -d= -f2)
|
|
506
|
+
if [[ -n "$oom_val" ]]; then
|
|
507
|
+
current_value="$oom_val"
|
|
508
|
+
fi
|
|
509
|
+
fi
|
|
510
|
+
;;
|
|
511
|
+
*)
|
|
512
|
+
if [[ -f "$config_file" ]]; then
|
|
513
|
+
current_value="(file exists)"
|
|
514
|
+
else
|
|
515
|
+
current_value="(file missing)"
|
|
516
|
+
fi
|
|
517
|
+
;;
|
|
518
|
+
esac
|
|
519
|
+
|
|
520
|
+
# Notes column
|
|
521
|
+
local notes=""
|
|
522
|
+
if [[ "$requires_reboot" == "true" ]]; then
|
|
523
|
+
notes="[REBOOT]"
|
|
524
|
+
fi
|
|
525
|
+
|
|
526
|
+
# jemalloc skip check
|
|
527
|
+
if [[ "$id" == "MEM-019" ]] && ! $has_jemalloc; then
|
|
528
|
+
notes="${notes} [SKIP: library not found]"
|
|
529
|
+
fi
|
|
530
|
+
|
|
531
|
+
# Service existence check
|
|
532
|
+
if [[ "$config_file" == "systemctl disable/mask" && "${svc_exists_cache[$parameter]:-no}" == "no" ]]; then
|
|
533
|
+
notes="${notes} [SKIP: service not installed]"
|
|
534
|
+
fi
|
|
535
|
+
|
|
536
|
+
# CPU-003 is "keep active" — informational
|
|
537
|
+
if [[ "$config_file" == "systemctl" && "$after_value" == *"kept"* ]]; then
|
|
538
|
+
notes="${notes} [INFO: no change]"
|
|
539
|
+
fi
|
|
540
|
+
|
|
541
|
+
printf "%-12s %-42s %-40s %-20s → %-20s %s\n" \
|
|
542
|
+
"$id" "${parameter:0:42}" "${config_file:0:40}" "${current_value:0:20}" "${target_value:0:20}" "$notes"
|
|
543
|
+
|
|
544
|
+
done <<< "$tsv_data"
|
|
545
|
+
|
|
546
|
+
echo ""
|
|
547
|
+
echo "============================================"
|
|
548
|
+
echo " DRY RUN complete — no changes made"
|
|
549
|
+
echo " To apply: tune.sh --tier $TIER --apply"
|
|
550
|
+
echo "============================================"
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
# -------------------------------------------------------
|
|
554
|
+
# APPLY: Actually apply tuning changes
|
|
555
|
+
# -------------------------------------------------------
|
|
556
|
+
do_apply() {
|
|
557
|
+
local ram_kb
|
|
558
|
+
ram_kb=$(detect_ram_kb)
|
|
559
|
+
|
|
560
|
+
local filter
|
|
561
|
+
filter=$(get_tier_filter)
|
|
562
|
+
|
|
563
|
+
# Extract entries as TSV
|
|
564
|
+
local tsv_data
|
|
565
|
+
tsv_data=$(jq -r "[.tuning_entries[] | $filter] | .[] | [.id, .category, .parameter, .config_file, .before_value, .after_value, .risk, (.requires_reboot | tostring), (.scaling_note // \"\")] | @tsv" "$MANIFEST")
|
|
566
|
+
|
|
567
|
+
local count
|
|
568
|
+
count=$(echo "$tsv_data" | wc -l)
|
|
569
|
+
|
|
570
|
+
echo "============================================"
|
|
571
|
+
echo " tunectl tune apply — Tier: $TIER"
|
|
572
|
+
echo "============================================"
|
|
573
|
+
echo ""
|
|
574
|
+
echo "Applying $count entries..."
|
|
575
|
+
echo ""
|
|
576
|
+
|
|
577
|
+
# --- Step 1: Create timestamped backup ---
|
|
578
|
+
local timestamp
|
|
579
|
+
timestamp=$(date +%Y-%m-%dT%H-%M-%S)
|
|
580
|
+
local backup_dir="${BACKUP_ROOT}/${timestamp}"
|
|
581
|
+
mkdir -p "$backup_dir"
|
|
582
|
+
echo "Backup directory: $backup_dir"
|
|
583
|
+
|
|
584
|
+
# Collect unique config files that will be modified (exclude systemctl pseudo-files)
|
|
585
|
+
local config_files_list
|
|
586
|
+
config_files_list=$(echo "$tsv_data" | awk -F'\t' '{print $4}' | sort -u | grep -v '^systemctl')
|
|
587
|
+
|
|
588
|
+
while IFS= read -r cf; do
|
|
589
|
+
[[ -z "$cf" ]] && continue
|
|
590
|
+
if [[ -f "$cf" ]]; then
|
|
591
|
+
local backup_path="${backup_dir}${cf}"
|
|
592
|
+
mkdir -p "$(dirname "$backup_path")"
|
|
593
|
+
cp -p "$cf" "$backup_path"
|
|
594
|
+
echo " Backed up: $cf"
|
|
595
|
+
fi
|
|
596
|
+
done <<< "$config_files_list"
|
|
597
|
+
echo ""
|
|
598
|
+
|
|
599
|
+
# --- Step 2: Collect and write sysctl entries ---
|
|
600
|
+
local sysctl_lines
|
|
601
|
+
sysctl_lines=$(echo "$tsv_data" | awk -F'\t' '$4 == "/etc/sysctl.d/99-performance.conf"')
|
|
602
|
+
local sysctl_count
|
|
603
|
+
sysctl_count=$(echo "$sysctl_lines" | grep -c . || true)
|
|
604
|
+
|
|
605
|
+
if [[ $sysctl_count -gt 0 ]]; then
|
|
606
|
+
echo "Writing sysctl values to /etc/sysctl.d/99-performance.conf..."
|
|
607
|
+
local sysctl_file="/etc/sysctl.d/99-performance.conf"
|
|
608
|
+
|
|
609
|
+
{
|
|
610
|
+
echo "# tunectl performance tuning — managed by tunectl"
|
|
611
|
+
echo "# Tier: $TIER ($sysctl_count sysctl entries)"
|
|
612
|
+
echo ""
|
|
613
|
+
|
|
614
|
+
local prev_category=""
|
|
615
|
+
while IFS=$'\t' read -r id category parameter config_file before_value after_value risk requires_reboot scaling_note; do
|
|
616
|
+
local target_value
|
|
617
|
+
target_value=$(scale_value "$id" "$after_value" "$ram_kb")
|
|
618
|
+
|
|
619
|
+
if [[ "$category" != "$prev_category" ]]; then
|
|
620
|
+
[[ -n "$prev_category" ]] && echo ""
|
|
621
|
+
echo "# ${category} tuning"
|
|
622
|
+
prev_category="$category"
|
|
623
|
+
fi
|
|
624
|
+
|
|
625
|
+
echo "$parameter = $target_value"
|
|
626
|
+
done <<< "$sysctl_lines"
|
|
627
|
+
} > "$sysctl_file"
|
|
628
|
+
|
|
629
|
+
echo " Written $sysctl_count sysctl parameters"
|
|
630
|
+
|
|
631
|
+
if sysctl --system >/dev/null 2>&1; then
|
|
632
|
+
echo " Sysctl values reloaded"
|
|
633
|
+
else
|
|
634
|
+
echo " Warning: sysctl reload returned non-zero (some params may have failed)" >&2
|
|
635
|
+
fi
|
|
636
|
+
echo ""
|
|
637
|
+
fi
|
|
638
|
+
|
|
639
|
+
# --- Step 3: Write other config files ---
|
|
640
|
+
local grub_changed=false
|
|
641
|
+
|
|
642
|
+
# Pre-check services
|
|
643
|
+
local all_units
|
|
644
|
+
all_units=$( (systemctl list-unit-files --type=service --no-legend 2>/dev/null; systemctl list-unit-files --type=socket --no-legend 2>/dev/null) | awk '{print $1}' || echo "")
|
|
645
|
+
|
|
646
|
+
local has_jemalloc=false
|
|
647
|
+
check_jemalloc && has_jemalloc=true
|
|
648
|
+
|
|
649
|
+
while IFS=$'\t' read -r id category parameter config_file before_value after_value risk requires_reboot scaling_note; do
|
|
650
|
+
local target_value
|
|
651
|
+
target_value=$(scale_value "$id" "$after_value" "$ram_kb")
|
|
652
|
+
|
|
653
|
+
case "$config_file" in
|
|
654
|
+
/etc/sysctl.d/99-performance.conf)
|
|
655
|
+
# Already handled above
|
|
656
|
+
;;
|
|
657
|
+
|
|
658
|
+
"systemctl disable/mask")
|
|
659
|
+
if echo "$all_units" | grep -qF "$parameter"; then
|
|
660
|
+
systemctl disable "$parameter" 2>/dev/null || true
|
|
661
|
+
systemctl mask "$parameter" 2>/dev/null || true
|
|
662
|
+
systemctl stop "$parameter" 2>/dev/null || true
|
|
663
|
+
echo " $id: disabled/masked $parameter"
|
|
664
|
+
else
|
|
665
|
+
echo " $id: SKIP — $parameter not installed"
|
|
666
|
+
fi
|
|
667
|
+
;;
|
|
668
|
+
|
|
669
|
+
"systemctl")
|
|
670
|
+
echo " $id: $parameter — no change (keep current state)"
|
|
671
|
+
;;
|
|
672
|
+
|
|
673
|
+
/etc/environment)
|
|
674
|
+
case "$id" in
|
|
675
|
+
MEM-019)
|
|
676
|
+
if $has_jemalloc; then
|
|
677
|
+
if [[ -f /etc/environment ]] && grep -q '^LD_PRELOAD=' /etc/environment; then
|
|
678
|
+
sed -i "s|^LD_PRELOAD=.*|LD_PRELOAD=$target_value|" /etc/environment
|
|
679
|
+
else
|
|
680
|
+
echo "LD_PRELOAD=$target_value" >> /etc/environment
|
|
681
|
+
fi
|
|
682
|
+
echo " $id: Set LD_PRELOAD=$target_value"
|
|
683
|
+
else
|
|
684
|
+
echo " $id: SKIP — jemalloc library not found"
|
|
685
|
+
fi
|
|
686
|
+
;;
|
|
687
|
+
BUILD-001)
|
|
688
|
+
if command -v sccache &>/dev/null; then
|
|
689
|
+
if [[ -f /etc/environment ]] && grep -q '^RUSTC_WRAPPER=' /etc/environment; then
|
|
690
|
+
sed -i "s|^RUSTC_WRAPPER=.*|RUSTC_WRAPPER=$target_value|" /etc/environment
|
|
691
|
+
else
|
|
692
|
+
echo "RUSTC_WRAPPER=$target_value" >> /etc/environment
|
|
693
|
+
fi
|
|
694
|
+
echo " $id: Set RUSTC_WRAPPER=$target_value"
|
|
695
|
+
else
|
|
696
|
+
echo " $id: SKIP — sccache not found in PATH"
|
|
697
|
+
fi
|
|
698
|
+
;;
|
|
699
|
+
esac
|
|
700
|
+
;;
|
|
701
|
+
|
|
702
|
+
/etc/default/grub)
|
|
703
|
+
grub_changed=true
|
|
704
|
+
handle_grub_entry "$id" "$parameter" "$target_value"
|
|
705
|
+
;;
|
|
706
|
+
|
|
707
|
+
/etc/fstab)
|
|
708
|
+
handle_fstab_entry "$id" "$parameter" "$target_value" "$ram_kb"
|
|
709
|
+
;;
|
|
710
|
+
|
|
711
|
+
/etc/modules-load.d/*)
|
|
712
|
+
mkdir -p "$(dirname "$config_file")"
|
|
713
|
+
echo "$target_value" > "$config_file"
|
|
714
|
+
echo " $id: Written $config_file ($target_value)"
|
|
715
|
+
;;
|
|
716
|
+
|
|
717
|
+
/etc/udev/rules.d/*)
|
|
718
|
+
handle_udev_entry "$id" "$config_file" "$parameter" "$target_value"
|
|
719
|
+
;;
|
|
720
|
+
|
|
721
|
+
/etc/tmpfiles.d/*)
|
|
722
|
+
handle_tmpfiles_entry "$id" "$config_file" "$parameter" "$target_value"
|
|
723
|
+
;;
|
|
724
|
+
|
|
725
|
+
/etc/systemd/system/*.slice)
|
|
726
|
+
handle_slice_entry "$id" "$config_file" "$parameter" "$target_value"
|
|
727
|
+
;;
|
|
728
|
+
|
|
729
|
+
/usr/local/bin/*)
|
|
730
|
+
handle_helper_script "$id" "$config_file" "$parameter" "$target_value"
|
|
731
|
+
;;
|
|
732
|
+
|
|
733
|
+
*)
|
|
734
|
+
echo " $id: Unknown config file type: $config_file (skipped)" >&2
|
|
735
|
+
;;
|
|
736
|
+
esac
|
|
737
|
+
done <<< "$tsv_data"
|
|
738
|
+
|
|
739
|
+
# --- Step 4: Run update-grub if GRUB entries changed ---
|
|
740
|
+
if $grub_changed; then
|
|
741
|
+
echo ""
|
|
742
|
+
echo "GRUB entries changed — running update-grub..."
|
|
743
|
+
if command -v update-grub &>/dev/null; then
|
|
744
|
+
update-grub 2>/dev/null || echo " Warning: update-grub returned non-zero" >&2
|
|
745
|
+
else
|
|
746
|
+
echo " Warning: update-grub not found" >&2
|
|
747
|
+
fi
|
|
748
|
+
fi
|
|
749
|
+
|
|
750
|
+
# --- Step 5: Reload systemd if slice files changed ---
|
|
751
|
+
systemctl daemon-reload 2>/dev/null || true
|
|
752
|
+
|
|
753
|
+
echo ""
|
|
754
|
+
echo "============================================"
|
|
755
|
+
echo " Apply complete — $count entries processed"
|
|
756
|
+
echo " Backup saved: $backup_dir"
|
|
757
|
+
echo "============================================"
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
# -------------------------------------------------------
|
|
761
|
+
# Handle GRUB kernel command line entries
|
|
762
|
+
# -------------------------------------------------------
|
|
763
|
+
handle_grub_entry() {
|
|
764
|
+
local id="$1"
|
|
765
|
+
local parameter="$2"
|
|
766
|
+
local target_value="$3"
|
|
767
|
+
local grub_file="/etc/default/grub"
|
|
768
|
+
|
|
769
|
+
if [[ ! -f "$grub_file" ]]; then
|
|
770
|
+
echo " $id: SKIP — $grub_file not found" >&2
|
|
771
|
+
return
|
|
772
|
+
fi
|
|
773
|
+
|
|
774
|
+
local param_name="" param_str=""
|
|
775
|
+
case "$id" in
|
|
776
|
+
SWAP-009) param_name="zswap.enabled"; param_str="zswap.enabled=0" ;;
|
|
777
|
+
BOOT-001) param_name="mitigations"; param_str="mitigations=auto,nosmt" ;;
|
|
778
|
+
BOOT-002) param_name="l1tf"; param_str="l1tf=off" ;;
|
|
779
|
+
BOOT-003) param_name="tsx_async_abort"; param_str="tsx_async_abort=off" ;;
|
|
780
|
+
BOOT-004) param_name="preempt"; param_str="preempt=none" ;;
|
|
781
|
+
BOOT-005) param_name="transparent_hugepage"; param_str="transparent_hugepage=always" ;;
|
|
782
|
+
*)
|
|
783
|
+
echo " $id: Unknown GRUB parameter" >&2
|
|
784
|
+
return
|
|
785
|
+
;;
|
|
786
|
+
esac
|
|
787
|
+
|
|
788
|
+
local current_cmdline
|
|
789
|
+
current_cmdline=$(grep '^GRUB_CMDLINE_LINUX_DEFAULT=' "$grub_file" | sed 's/^GRUB_CMDLINE_LINUX_DEFAULT=//' | tr -d '"' || echo "")
|
|
790
|
+
|
|
791
|
+
local new_cmdline
|
|
792
|
+
new_cmdline=$(echo "$current_cmdline" | sed -E "s/(^| )${param_name}=[^ ]*//" | sed 's/ */ /g' | sed 's/^ //;s/ $//')
|
|
793
|
+
|
|
794
|
+
if [[ -n "$new_cmdline" ]]; then
|
|
795
|
+
new_cmdline="$new_cmdline $param_str"
|
|
796
|
+
else
|
|
797
|
+
new_cmdline="$param_str"
|
|
798
|
+
fi
|
|
799
|
+
|
|
800
|
+
if grep -q '^GRUB_CMDLINE_LINUX_DEFAULT=' "$grub_file"; then
|
|
801
|
+
sed -i "s|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT=\"$new_cmdline\"|" "$grub_file"
|
|
802
|
+
else
|
|
803
|
+
echo "GRUB_CMDLINE_LINUX_DEFAULT=\"$new_cmdline\"" >> "$grub_file"
|
|
804
|
+
fi
|
|
805
|
+
|
|
806
|
+
echo " $id: GRUB $param_name set ($param_str)"
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
# -------------------------------------------------------
|
|
810
|
+
# Handle fstab entries
|
|
811
|
+
# -------------------------------------------------------
|
|
812
|
+
handle_fstab_entry() {
|
|
813
|
+
local id="$1"
|
|
814
|
+
local parameter="$2"
|
|
815
|
+
local target_value="$3"
|
|
816
|
+
local ram_kb="$4"
|
|
817
|
+
local fstab="/etc/fstab"
|
|
818
|
+
|
|
819
|
+
case "$id" in
|
|
820
|
+
SWAP-005)
|
|
821
|
+
if ! grep -q '/dev/zram0' "$fstab" 2>/dev/null; then
|
|
822
|
+
echo "$target_value" >> "$fstab"
|
|
823
|
+
echo " $id: Added zram0 swap entry to fstab"
|
|
824
|
+
else
|
|
825
|
+
echo " $id: zram0 already in fstab (idempotent)"
|
|
826
|
+
fi
|
|
827
|
+
;;
|
|
828
|
+
FS-001)
|
|
829
|
+
if grep -qE '^\S+\s+/\s' "$fstab"; then
|
|
830
|
+
if ! grep -E '^\S+\s+/\s' "$fstab" | grep -q 'noatime'; then
|
|
831
|
+
sed -i -E '/^\S+\s+\/\s/s/(defaults[^ ]*)/\1,noatime/' "$fstab"
|
|
832
|
+
echo " $id: Added noatime to root mount"
|
|
833
|
+
else
|
|
834
|
+
echo " $id: noatime already set (idempotent)"
|
|
835
|
+
fi
|
|
836
|
+
else
|
|
837
|
+
echo " $id: SKIP — root mount not found in fstab"
|
|
838
|
+
fi
|
|
839
|
+
;;
|
|
840
|
+
FS-002)
|
|
841
|
+
if grep -qE '^\S+\s+/\s' "$fstab"; then
|
|
842
|
+
if ! grep -E '^\S+\s+/\s' "$fstab" | grep -q 'commit='; then
|
|
843
|
+
sed -i -E '/^\S+\s+\/\s/s/(defaults[^ ]*)/\1,commit=60/' "$fstab"
|
|
844
|
+
echo " $id: Added commit=60 to root mount"
|
|
845
|
+
else
|
|
846
|
+
echo " $id: commit interval already set (idempotent)"
|
|
847
|
+
fi
|
|
848
|
+
else
|
|
849
|
+
echo " $id: SKIP — root mount not found in fstab"
|
|
850
|
+
fi
|
|
851
|
+
;;
|
|
852
|
+
FS-003)
|
|
853
|
+
if ! grep -q 'tmpfs\s\+/tmp' "$fstab" 2>/dev/null; then
|
|
854
|
+
echo "$target_value" >> "$fstab"
|
|
855
|
+
echo " $id: Added tmpfs /tmp entry"
|
|
856
|
+
else
|
|
857
|
+
echo " $id: tmpfs /tmp already in fstab (idempotent)"
|
|
858
|
+
fi
|
|
859
|
+
;;
|
|
860
|
+
*)
|
|
861
|
+
echo " $id: Unknown fstab entry" >&2
|
|
862
|
+
;;
|
|
863
|
+
esac
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
# -------------------------------------------------------
|
|
867
|
+
# Handle udev rule entries
|
|
868
|
+
# -------------------------------------------------------
|
|
869
|
+
handle_udev_entry() {
|
|
870
|
+
local id="$1"
|
|
871
|
+
local config_file="$2"
|
|
872
|
+
local parameter="$3"
|
|
873
|
+
local target_value="$4"
|
|
874
|
+
|
|
875
|
+
mkdir -p "$(dirname "$config_file")"
|
|
876
|
+
|
|
877
|
+
case "$id" in
|
|
878
|
+
FS-004)
|
|
879
|
+
echo 'ACTION=="add|change", KERNEL=="vd[a-z]|sd[a-z]|nvme[0-9]*", ATTR{queue/scheduler}="none"' > "$config_file"
|
|
880
|
+
echo " $id: Written I/O scheduler udev rule"
|
|
881
|
+
;;
|
|
882
|
+
FS-005)
|
|
883
|
+
local rule='ACTION=="add|change", KERNEL=="vd[a-z]|sd[a-z]|nvme[0-9]*", ATTR{queue/read_ahead_kb}="1024"'
|
|
884
|
+
if [[ -f "$config_file" ]] && ! grep -q 'read_ahead_kb' "$config_file"; then
|
|
885
|
+
echo "$rule" >> "$config_file"
|
|
886
|
+
elif [[ ! -f "$config_file" ]]; then
|
|
887
|
+
echo "$rule" > "$config_file"
|
|
888
|
+
fi
|
|
889
|
+
echo " $id: Written read-ahead udev rule"
|
|
890
|
+
;;
|
|
891
|
+
SWAP-006)
|
|
892
|
+
local rule="KERNEL==\"zram0\", ATTR{comp_algorithm}=\"$target_value\""
|
|
893
|
+
if [[ -f "$config_file" ]] && grep -q 'comp_algorithm' "$config_file"; then
|
|
894
|
+
sed -i "s|comp_algorithm.*|comp_algorithm}=\"$target_value\"|" "$config_file"
|
|
895
|
+
elif [[ -f "$config_file" ]]; then
|
|
896
|
+
echo "$rule" >> "$config_file"
|
|
897
|
+
else
|
|
898
|
+
echo "$rule" > "$config_file"
|
|
899
|
+
fi
|
|
900
|
+
echo " $id: Written zram comp_algorithm rule"
|
|
901
|
+
;;
|
|
902
|
+
SWAP-007)
|
|
903
|
+
local rule="KERNEL==\"zram0\", ATTR{disksize}=\"$target_value\""
|
|
904
|
+
if [[ -f "$config_file" ]] && grep -q 'disksize' "$config_file"; then
|
|
905
|
+
sed -i "s|disksize.*|disksize}=\"$target_value\"|" "$config_file"
|
|
906
|
+
elif [[ -f "$config_file" ]]; then
|
|
907
|
+
echo "$rule" >> "$config_file"
|
|
908
|
+
else
|
|
909
|
+
echo "$rule" > "$config_file"
|
|
910
|
+
fi
|
|
911
|
+
echo " $id: Written zram disksize rule ($target_value)"
|
|
912
|
+
;;
|
|
913
|
+
*)
|
|
914
|
+
echo " $id: Unhandled udev entry" >&2
|
|
915
|
+
;;
|
|
916
|
+
esac
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
# -------------------------------------------------------
|
|
920
|
+
# Handle tmpfiles.d entries — manifest-driven, writes per-entry line
|
|
921
|
+
# Uses a tracking variable to write header + all entries on first call per file.
|
|
922
|
+
# -------------------------------------------------------
|
|
923
|
+
declare -A _tmpfiles_written=()
|
|
924
|
+
|
|
925
|
+
handle_tmpfiles_entry() {
|
|
926
|
+
local id="$1"
|
|
927
|
+
local config_file="$2"
|
|
928
|
+
local parameter="$3"
|
|
929
|
+
local target_value="$4"
|
|
930
|
+
|
|
931
|
+
mkdir -p "$(dirname "$config_file")"
|
|
932
|
+
|
|
933
|
+
# Write the complete file on first entry for this config_file.
|
|
934
|
+
# All values are read from the manifest via the filtered tsv_data.
|
|
935
|
+
if [[ -z "${_tmpfiles_written[$config_file]:-}" ]]; then
|
|
936
|
+
_tmpfiles_written["$config_file"]=1
|
|
937
|
+
|
|
938
|
+
# Determine sys path prefix and header based on file
|
|
939
|
+
local header=""
|
|
940
|
+
local sys_prefix=""
|
|
941
|
+
case "$config_file" in
|
|
942
|
+
/etc/tmpfiles.d/thp.conf)
|
|
943
|
+
header="# Transparent Huge Pages configuration"
|
|
944
|
+
sys_prefix="/sys/kernel/mm/"
|
|
945
|
+
;;
|
|
946
|
+
/etc/tmpfiles.d/ksm.conf)
|
|
947
|
+
header="# KSM (Kernel Samepage Merging) configuration"
|
|
948
|
+
sys_prefix="/sys/kernel/mm/"
|
|
949
|
+
;;
|
|
950
|
+
esac
|
|
951
|
+
|
|
952
|
+
{
|
|
953
|
+
echo "$header"
|
|
954
|
+
# Extract all entries for this config_file from the manifest, apply scaling
|
|
955
|
+
local file_entries
|
|
956
|
+
file_entries=$(jq -r --arg cf "$config_file" \
|
|
957
|
+
'[.tuning_entries[] | select(.config_file == $cf)] | .[] | [.id, .parameter, .after_value] | @tsv' \
|
|
958
|
+
"$MANIFEST")
|
|
959
|
+
local ram_kb
|
|
960
|
+
ram_kb=$(detect_ram_kb)
|
|
961
|
+
while IFS=$'\t' read -r eid eparam eafter; do
|
|
962
|
+
[[ -z "$eid" ]] && continue
|
|
963
|
+
local scaled_val
|
|
964
|
+
scaled_val=$(scale_value "$eid" "$eafter" "$ram_kb")
|
|
965
|
+
echo "w ${sys_prefix}${eparam} - - - - ${scaled_val}"
|
|
966
|
+
done <<< "$file_entries"
|
|
967
|
+
} > "$config_file"
|
|
968
|
+
echo " $id: Written ${config_file} (all entries from manifest)"
|
|
969
|
+
else
|
|
970
|
+
echo " $id: ${config_file} already written"
|
|
971
|
+
fi
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
# -------------------------------------------------------
|
|
975
|
+
# Handle systemd slice entries — manifest-driven, writes per-slice file
|
|
976
|
+
# Writes all properties for the slice from manifest on first call per file.
|
|
977
|
+
# -------------------------------------------------------
|
|
978
|
+
declare -A _slice_written=()
|
|
979
|
+
|
|
980
|
+
handle_slice_entry() {
|
|
981
|
+
local id="$1"
|
|
982
|
+
local config_file="$2"
|
|
983
|
+
local parameter="$3"
|
|
984
|
+
local target_value="$4"
|
|
985
|
+
|
|
986
|
+
mkdir -p "$(dirname "$config_file")"
|
|
987
|
+
|
|
988
|
+
# Write the complete slice file on first entry for this config_file.
|
|
989
|
+
if [[ -z "${_slice_written[$config_file]:-}" ]]; then
|
|
990
|
+
_slice_written["$config_file"]=1
|
|
991
|
+
|
|
992
|
+
local ram_kb
|
|
993
|
+
ram_kb=$(detect_ram_kb)
|
|
994
|
+
|
|
995
|
+
{
|
|
996
|
+
echo "[Slice]"
|
|
997
|
+
# Extract all entries for this config_file from the manifest
|
|
998
|
+
local file_entries
|
|
999
|
+
file_entries=$(jq -r --arg cf "$config_file" \
|
|
1000
|
+
'[.tuning_entries[] | select(.config_file == $cf)] | .[] | [.id, .parameter, .after_value] | @tsv' \
|
|
1001
|
+
"$MANIFEST")
|
|
1002
|
+
while IFS=$'\t' read -r eid eparam eafter; do
|
|
1003
|
+
[[ -z "$eid" ]] && continue
|
|
1004
|
+
local scaled_val
|
|
1005
|
+
scaled_val=$(scale_value "$eid" "$eafter" "$ram_kb")
|
|
1006
|
+
# Extract the property name from the parameter field (e.g., "droid.slice CPUWeight" -> "CPUWeight")
|
|
1007
|
+
local prop_name
|
|
1008
|
+
prop_name=$(echo "$eparam" | awk '{print $NF}')
|
|
1009
|
+
echo "${prop_name}=${scaled_val}"
|
|
1010
|
+
done <<< "$file_entries"
|
|
1011
|
+
} > "$config_file"
|
|
1012
|
+
echo " $id: Written ${config_file} (all entries from manifest)"
|
|
1013
|
+
else
|
|
1014
|
+
echo " $id: ${config_file} already written"
|
|
1015
|
+
fi
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
# -------------------------------------------------------
|
|
1019
|
+
# Handle helper scripts (run-in-droid, run-in-bulk) — manifest-driven
|
|
1020
|
+
# OOMScoreAdjust value comes from the manifest's after_value field.
|
|
1021
|
+
# -------------------------------------------------------
|
|
1022
|
+
handle_helper_script() {
|
|
1023
|
+
local id="$1"
|
|
1024
|
+
local config_file="$2"
|
|
1025
|
+
local parameter="$3"
|
|
1026
|
+
local target_value="$4"
|
|
1027
|
+
|
|
1028
|
+
mkdir -p "$(dirname "$config_file")"
|
|
1029
|
+
|
|
1030
|
+
# Determine slice name from the config_file path
|
|
1031
|
+
local slice_name=""
|
|
1032
|
+
local script_desc=""
|
|
1033
|
+
case "$config_file" in
|
|
1034
|
+
*/run-in-droid)
|
|
1035
|
+
slice_name="droid.slice"
|
|
1036
|
+
script_desc="Run a command in the droid.slice cgroup with OOM protection"
|
|
1037
|
+
;;
|
|
1038
|
+
*/run-in-bulk)
|
|
1039
|
+
slice_name="bulk.slice"
|
|
1040
|
+
script_desc="Run a command in the bulk.slice cgroup (OOM-expendable)"
|
|
1041
|
+
;;
|
|
1042
|
+
*)
|
|
1043
|
+
echo " $id: Unknown helper script: $config_file" >&2
|
|
1044
|
+
return
|
|
1045
|
+
;;
|
|
1046
|
+
esac
|
|
1047
|
+
|
|
1048
|
+
cat > "$config_file" <<SCRIPT
|
|
1049
|
+
#!/usr/bin/env bash
|
|
1050
|
+
# $(basename "$config_file"): ${script_desc}
|
|
1051
|
+
exec systemd-run --slice=${slice_name} --scope --property=OOMScoreAdjust=${target_value} -- "\$@"
|
|
1052
|
+
SCRIPT
|
|
1053
|
+
chmod +x "$config_file"
|
|
1054
|
+
echo " $id: Written $config_file (OOMScoreAdjust=$target_value)"
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
# -------------------------------------------------------
|
|
1058
|
+
# Main
|
|
1059
|
+
# -------------------------------------------------------
|
|
1060
|
+
main() {
|
|
1061
|
+
parse_args "$@"
|
|
1062
|
+
validate_manifest
|
|
1063
|
+
|
|
1064
|
+
if [[ "$MODE" == "dry-run" ]]; then
|
|
1065
|
+
do_dry_run
|
|
1066
|
+
else
|
|
1067
|
+
do_apply
|
|
1068
|
+
fi
|
|
1069
|
+
|
|
1070
|
+
exit 0
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
main "$@"
|