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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# discover.sh — Environment detection script for tunectl
|
|
5
|
+
# Outputs a JSON report to stdout with system information.
|
|
6
|
+
# Strictly read-only: no system modifications.
|
|
7
|
+
# Works without root privileges.
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
|
|
11
|
+
# -------------------------------------------------------
|
|
12
|
+
# Helper: safe file read (returns empty string if missing)
|
|
13
|
+
# -------------------------------------------------------
|
|
14
|
+
safe_read() {
|
|
15
|
+
local file="$1"
|
|
16
|
+
if [[ -r "$file" ]]; then
|
|
17
|
+
cat "$file" 2>/dev/null || echo ""
|
|
18
|
+
else
|
|
19
|
+
echo ""
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# -------------------------------------------------------
|
|
24
|
+
# Detect OS version from /etc/os-release
|
|
25
|
+
# -------------------------------------------------------
|
|
26
|
+
detect_os_version() {
|
|
27
|
+
if [[ -r /etc/os-release ]]; then
|
|
28
|
+
# shellcheck disable=SC1091
|
|
29
|
+
. /etc/os-release 2>/dev/null
|
|
30
|
+
echo "${PRETTY_NAME:-unknown}"
|
|
31
|
+
else
|
|
32
|
+
echo "unknown"
|
|
33
|
+
fi
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# -------------------------------------------------------
|
|
37
|
+
# Detect kernel version via uname -r
|
|
38
|
+
# -------------------------------------------------------
|
|
39
|
+
detect_kernel_version() {
|
|
40
|
+
uname -r 2>/dev/null || echo "unknown"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# -------------------------------------------------------
|
|
44
|
+
# Detect RAM in MB from /proc/meminfo
|
|
45
|
+
# -------------------------------------------------------
|
|
46
|
+
detect_ram_mb() {
|
|
47
|
+
local mem_kb
|
|
48
|
+
mem_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo "0")
|
|
49
|
+
echo $(( mem_kb / 1024 ))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# -------------------------------------------------------
|
|
53
|
+
# Detect CPU count via nproc
|
|
54
|
+
# -------------------------------------------------------
|
|
55
|
+
detect_cpu_count() {
|
|
56
|
+
nproc 2>/dev/null || grep -c '^processor' /proc/cpuinfo 2>/dev/null || echo "1"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# -------------------------------------------------------
|
|
60
|
+
# Detect disk type: check /sys/block/*/queue/rotational
|
|
61
|
+
# and look for virtio driver
|
|
62
|
+
# -------------------------------------------------------
|
|
63
|
+
detect_disk_type() {
|
|
64
|
+
local disk_type="unknown"
|
|
65
|
+
# Find the primary block device (skip loop, zram, dm, sr, fd devices)
|
|
66
|
+
local primary_disk=""
|
|
67
|
+
for dev in /sys/block/*; do
|
|
68
|
+
local devname
|
|
69
|
+
devname="$(basename "$dev")"
|
|
70
|
+
# Skip non-physical devices
|
|
71
|
+
case "$devname" in
|
|
72
|
+
loop*|zram*|dm-*|sr*|fd*|ram*) continue ;;
|
|
73
|
+
esac
|
|
74
|
+
primary_disk="$devname"
|
|
75
|
+
break
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
if [[ -z "$primary_disk" ]]; then
|
|
79
|
+
echo "unknown"
|
|
80
|
+
return
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Check for virtio driver
|
|
84
|
+
local driver_path="/sys/block/${primary_disk}/device/driver"
|
|
85
|
+
if [[ -L "$driver_path" ]]; then
|
|
86
|
+
local driver_target
|
|
87
|
+
driver_target="$(readlink "$driver_path" 2>/dev/null || echo "")"
|
|
88
|
+
if [[ "$driver_target" == *"virtio"* ]]; then
|
|
89
|
+
disk_type="virtio"
|
|
90
|
+
echo "$disk_type"
|
|
91
|
+
return
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Check modalias for virtio
|
|
96
|
+
local modalias
|
|
97
|
+
modalias="$(safe_read "/sys/block/${primary_disk}/device/modalias")"
|
|
98
|
+
if [[ "$modalias" == *"virtio"* ]]; then
|
|
99
|
+
disk_type="virtio"
|
|
100
|
+
echo "$disk_type"
|
|
101
|
+
return
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# Check rotational flag
|
|
105
|
+
local rotational
|
|
106
|
+
rotational="$(safe_read "/sys/block/${primary_disk}/queue/rotational")"
|
|
107
|
+
rotational="$(echo "$rotational" | tr -d '[:space:]')"
|
|
108
|
+
if [[ "$rotational" == "0" ]]; then
|
|
109
|
+
disk_type="ssd"
|
|
110
|
+
elif [[ "$rotational" == "1" ]]; then
|
|
111
|
+
# On cloud VPS, rotational=1 may still be SSD/virtio — check for common cloud patterns
|
|
112
|
+
# If we have virtio in the device name, it's likely a cloud SSD
|
|
113
|
+
if [[ "$primary_disk" == vd* ]]; then
|
|
114
|
+
disk_type="virtio"
|
|
115
|
+
else
|
|
116
|
+
disk_type="hdd"
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
echo "$disk_type"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# -------------------------------------------------------
|
|
124
|
+
# Detect virtualization type
|
|
125
|
+
# -------------------------------------------------------
|
|
126
|
+
detect_virt_type() {
|
|
127
|
+
# Try systemd-detect-virt first (most reliable)
|
|
128
|
+
if command -v systemd-detect-virt &>/dev/null; then
|
|
129
|
+
local virt
|
|
130
|
+
virt="$(systemd-detect-virt 2>/dev/null || echo "")"
|
|
131
|
+
if [[ -n "$virt" && "$virt" != "none" ]]; then
|
|
132
|
+
echo "$virt"
|
|
133
|
+
return
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Fallback: check /proc/cpuinfo for hypervisor flag
|
|
138
|
+
if grep -qi "hypervisor" /proc/cpuinfo 2>/dev/null; then
|
|
139
|
+
# Try to determine type from DMI
|
|
140
|
+
local product_name
|
|
141
|
+
product_name="$(safe_read /sys/class/dmi/id/product_name)"
|
|
142
|
+
case "$product_name" in
|
|
143
|
+
*"KVM"*|*"QEMU"*) echo "kvm"; return ;;
|
|
144
|
+
*"VMware"*) echo "vmware"; return ;;
|
|
145
|
+
*"VirtualBox"*) echo "oracle"; return ;;
|
|
146
|
+
*"Xen"*) echo "xen"; return ;;
|
|
147
|
+
esac
|
|
148
|
+
echo "vm"
|
|
149
|
+
return
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
echo "none"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# -------------------------------------------------------
|
|
156
|
+
# Detect swap state
|
|
157
|
+
# -------------------------------------------------------
|
|
158
|
+
detect_swap() {
|
|
159
|
+
local swap_total_kb=0
|
|
160
|
+
local has_zram=false
|
|
161
|
+
local has_file=false
|
|
162
|
+
local has_partition=false
|
|
163
|
+
|
|
164
|
+
# Read /proc/swaps
|
|
165
|
+
if [[ -r /proc/swaps ]]; then
|
|
166
|
+
while IFS= read -r line; do
|
|
167
|
+
# Skip header
|
|
168
|
+
[[ "$line" == Filename* ]] && continue
|
|
169
|
+
|
|
170
|
+
local filename type size
|
|
171
|
+
filename="$(echo "$line" | awk '{print $1}')"
|
|
172
|
+
type="$(echo "$line" | awk '{print $2}')"
|
|
173
|
+
size="$(echo "$line" | awk '{print $3}')"
|
|
174
|
+
|
|
175
|
+
swap_total_kb=$((swap_total_kb + size))
|
|
176
|
+
|
|
177
|
+
if [[ "$filename" == *"zram"* ]]; then
|
|
178
|
+
has_zram=true
|
|
179
|
+
elif [[ "$type" == "file" ]]; then
|
|
180
|
+
has_file=true
|
|
181
|
+
elif [[ "$type" == "partition" ]]; then
|
|
182
|
+
# zram shows as partition type too
|
|
183
|
+
if [[ "$filename" == *"zram"* ]]; then
|
|
184
|
+
has_zram=true
|
|
185
|
+
else
|
|
186
|
+
has_partition=true
|
|
187
|
+
fi
|
|
188
|
+
fi
|
|
189
|
+
done < /proc/swaps
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
local swap_configured=false
|
|
193
|
+
local swap_type="none"
|
|
194
|
+
local swap_total_mb=0
|
|
195
|
+
|
|
196
|
+
if [[ "$swap_total_kb" -gt 0 ]]; then
|
|
197
|
+
swap_configured=true
|
|
198
|
+
swap_total_mb=$((swap_total_kb / 1024))
|
|
199
|
+
|
|
200
|
+
if $has_zram; then
|
|
201
|
+
swap_type="zram"
|
|
202
|
+
elif $has_file || $has_partition; then
|
|
203
|
+
swap_type="file"
|
|
204
|
+
fi
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
echo "${swap_configured}|${swap_type}|${swap_total_mb}"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# -------------------------------------------------------
|
|
211
|
+
# Collect current sysctl values for key parameters
|
|
212
|
+
# -------------------------------------------------------
|
|
213
|
+
collect_sysctl_values() {
|
|
214
|
+
# Key sysctl parameters to check — extracted from manifest categories
|
|
215
|
+
local params=(
|
|
216
|
+
"vm.swappiness"
|
|
217
|
+
"vm.page-cluster"
|
|
218
|
+
"vm.dirty_ratio"
|
|
219
|
+
"vm.dirty_background_ratio"
|
|
220
|
+
"vm.dirty_expire_centisecs"
|
|
221
|
+
"vm.dirty_writeback_centisecs"
|
|
222
|
+
"vm.vfs_cache_pressure"
|
|
223
|
+
"vm.min_free_kbytes"
|
|
224
|
+
"vm.overcommit_memory"
|
|
225
|
+
"vm.max_map_count"
|
|
226
|
+
"vm.compaction_proactiveness"
|
|
227
|
+
"vm.numa_stat"
|
|
228
|
+
"vm.stat_interval"
|
|
229
|
+
"vm.watermark_boost_factor"
|
|
230
|
+
"vm.watermark_scale_factor"
|
|
231
|
+
"net.core.rmem_max"
|
|
232
|
+
"net.core.wmem_max"
|
|
233
|
+
"net.core.rmem_default"
|
|
234
|
+
"net.core.wmem_default"
|
|
235
|
+
"net.core.netdev_max_backlog"
|
|
236
|
+
"net.core.somaxconn"
|
|
237
|
+
"net.core.default_qdisc"
|
|
238
|
+
"net.ipv4.tcp_max_syn_backlog"
|
|
239
|
+
"net.ipv4.tcp_max_tw_buckets"
|
|
240
|
+
"net.ipv4.tcp_fin_timeout"
|
|
241
|
+
"net.ipv4.tcp_keepalive_time"
|
|
242
|
+
"net.ipv4.tcp_keepalive_intvl"
|
|
243
|
+
"net.ipv4.tcp_keepalive_probes"
|
|
244
|
+
"net.ipv4.tcp_slow_start_after_idle"
|
|
245
|
+
"net.ipv4.tcp_tw_reuse"
|
|
246
|
+
"net.ipv4.tcp_fastopen"
|
|
247
|
+
"net.ipv4.ip_local_port_range"
|
|
248
|
+
"net.ipv4.tcp_rmem"
|
|
249
|
+
"net.ipv4.tcp_wmem"
|
|
250
|
+
"net.ipv4.tcp_congestion_control"
|
|
251
|
+
"net.ipv4.tcp_mtu_probing"
|
|
252
|
+
"fs.inotify.max_user_watches"
|
|
253
|
+
"fs.inotify.max_user_instances"
|
|
254
|
+
"fs.aio-max-nr"
|
|
255
|
+
"kernel.sched_autogroup_enabled"
|
|
256
|
+
"kernel.sched_cfs_bandwidth_slice_us"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Build JSON object using jq for safe escaping
|
|
260
|
+
local json_obj="{}"
|
|
261
|
+
for param in "${params[@]}"; do
|
|
262
|
+
local value=""
|
|
263
|
+
# Try sysctl command first (handles name→path translation correctly)
|
|
264
|
+
value="$(sysctl -n "$param" 2>/dev/null || echo "")"
|
|
265
|
+
if [[ -z "$value" ]]; then
|
|
266
|
+
# Fallback: read from /proc/sys (convert dots to slashes)
|
|
267
|
+
local proc_path="/proc/sys/$(echo "$param" | tr '.' '/')"
|
|
268
|
+
if [[ -r "$proc_path" ]]; then
|
|
269
|
+
value="$(cat "$proc_path" 2>/dev/null | tr '\t' ' ')"
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
if [[ -n "$value" ]]; then
|
|
274
|
+
# Trim trailing whitespace
|
|
275
|
+
value="$(echo "$value" | sed 's/[[:space:]]*$//')"
|
|
276
|
+
json_obj="$(echo "$json_obj" | jq --arg k "$param" --arg v "$value" '. + {($k): $v}')"
|
|
277
|
+
fi
|
|
278
|
+
done
|
|
279
|
+
|
|
280
|
+
echo "$json_obj"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# -------------------------------------------------------
|
|
284
|
+
# Detect mount options for root partition
|
|
285
|
+
# -------------------------------------------------------
|
|
286
|
+
detect_mount_options() {
|
|
287
|
+
local mount_line
|
|
288
|
+
mount_line="$(mount 2>/dev/null | grep ' / ' | grep -v '//' | head -1)"
|
|
289
|
+
if [[ -n "$mount_line" ]]; then
|
|
290
|
+
# Extract options between parentheses
|
|
291
|
+
local opts
|
|
292
|
+
opts="$(echo "$mount_line" | sed 's/.*(\(.*\))/\1/')"
|
|
293
|
+
echo "$opts"
|
|
294
|
+
else
|
|
295
|
+
echo "unknown"
|
|
296
|
+
fi
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# -------------------------------------------------------
|
|
300
|
+
# Detect active services
|
|
301
|
+
# -------------------------------------------------------
|
|
302
|
+
detect_active_services() {
|
|
303
|
+
if command -v systemctl &>/dev/null; then
|
|
304
|
+
systemctl list-units --type=service --state=active --no-legend 2>/dev/null \
|
|
305
|
+
| awk '{print $1}' \
|
|
306
|
+
| jq -R -s 'split("\n") | map(select(length > 0))'
|
|
307
|
+
else
|
|
308
|
+
echo "[]"
|
|
309
|
+
fi
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# -------------------------------------------------------
|
|
313
|
+
# Main: collect all data and output JSON
|
|
314
|
+
# -------------------------------------------------------
|
|
315
|
+
main() {
|
|
316
|
+
local os_version kernel_version ram_mb cpu_count disk_type virt_type
|
|
317
|
+
local swap_info swap_configured swap_type swap_total_mb
|
|
318
|
+
local sysctl_values mount_options active_services
|
|
319
|
+
|
|
320
|
+
os_version="$(detect_os_version)"
|
|
321
|
+
kernel_version="$(detect_kernel_version)"
|
|
322
|
+
ram_mb="$(detect_ram_mb)"
|
|
323
|
+
cpu_count="$(detect_cpu_count)"
|
|
324
|
+
disk_type="$(detect_disk_type)"
|
|
325
|
+
virt_type="$(detect_virt_type)"
|
|
326
|
+
|
|
327
|
+
# Parse swap info (pipe-delimited)
|
|
328
|
+
swap_info="$(detect_swap)"
|
|
329
|
+
swap_configured="$(echo "$swap_info" | cut -d'|' -f1)"
|
|
330
|
+
swap_type="$(echo "$swap_info" | cut -d'|' -f2)"
|
|
331
|
+
swap_total_mb="$(echo "$swap_info" | cut -d'|' -f3)"
|
|
332
|
+
|
|
333
|
+
sysctl_values="$(collect_sysctl_values)"
|
|
334
|
+
mount_options="$(detect_mount_options)"
|
|
335
|
+
active_services="$(detect_active_services)"
|
|
336
|
+
|
|
337
|
+
# Build JSON output using jq for proper formatting and escaping
|
|
338
|
+
jq -n \
|
|
339
|
+
--arg os_version "$os_version" \
|
|
340
|
+
--arg kernel_version "$kernel_version" \
|
|
341
|
+
--argjson ram_mb "$ram_mb" \
|
|
342
|
+
--argjson cpu_count "$cpu_count" \
|
|
343
|
+
--arg disk_type "$disk_type" \
|
|
344
|
+
--arg virt_type "$virt_type" \
|
|
345
|
+
--argjson swap_configured "$swap_configured" \
|
|
346
|
+
--arg swap_type "$swap_type" \
|
|
347
|
+
--argjson swap_total_mb "$swap_total_mb" \
|
|
348
|
+
--argjson sysctl_values "$sysctl_values" \
|
|
349
|
+
--arg mount_options "$mount_options" \
|
|
350
|
+
--argjson active_services "$active_services" \
|
|
351
|
+
'{
|
|
352
|
+
os_version: $os_version,
|
|
353
|
+
kernel_version: $kernel_version,
|
|
354
|
+
ram_mb: $ram_mb,
|
|
355
|
+
cpu_count: $cpu_count,
|
|
356
|
+
disk_type: $disk_type,
|
|
357
|
+
virt_type: $virt_type,
|
|
358
|
+
swap_configured: $swap_configured,
|
|
359
|
+
swap_type: $swap_type,
|
|
360
|
+
swap_total_mb: $swap_total_mb,
|
|
361
|
+
sysctl_values: $sysctl_values,
|
|
362
|
+
mount_options: $mount_options,
|
|
363
|
+
active_services: $active_services
|
|
364
|
+
}'
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
main
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# rollback.sh — Restore system config from tunectl backup
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# rollback.sh [--list]
|
|
8
|
+
# rollback.sh [--backup <timestamp>]
|
|
9
|
+
#
|
|
10
|
+
# Modes:
|
|
11
|
+
# --list Show available backups (works without root)
|
|
12
|
+
# (default, no args) Restore from most recent backup (requires root)
|
|
13
|
+
# --backup <ts> Restore from specific backup timestamp (requires root)
|
|
14
|
+
#
|
|
15
|
+
# Environment overrides (for testing):
|
|
16
|
+
# BACKUP_ROOT Override backup directory path
|
|
17
|
+
# TUNECTL_SYSROOT Prefix for restore destination paths (sandbox)
|
|
18
|
+
# TUNECTL_SKIP_ROOT_CHECK Skip the root privilege check (set to 1)
|
|
19
|
+
# TUNECTL_SKIP_SYSCTL_RELOAD Skip sysctl --system reload (set to 1)
|
|
20
|
+
#
|
|
21
|
+
# Exit codes: 0=success, 1=operational failure, 2=usage error
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
BACKUP_ROOT="${BACKUP_ROOT:-/var/lib/tunectl/backups}"
|
|
25
|
+
|
|
26
|
+
# -------------------------------------------------------
|
|
27
|
+
# Usage / help
|
|
28
|
+
# -------------------------------------------------------
|
|
29
|
+
usage() {
|
|
30
|
+
cat >&2 <<EOF
|
|
31
|
+
Usage: rollback.sh [--list] [--backup <timestamp>]
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--list List available backups (does not require root)
|
|
35
|
+
--backup <ts> Restore from a specific backup timestamp
|
|
36
|
+
(no args) Restore from the most recent backup
|
|
37
|
+
--help Show this help message
|
|
38
|
+
|
|
39
|
+
Exit codes:
|
|
40
|
+
0 Success
|
|
41
|
+
1 Operational failure
|
|
42
|
+
2 Usage error
|
|
43
|
+
EOF
|
|
44
|
+
exit 2
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# -------------------------------------------------------
|
|
48
|
+
# Globals
|
|
49
|
+
# -------------------------------------------------------
|
|
50
|
+
MODE=""
|
|
51
|
+
BACKUP_TIMESTAMP=""
|
|
52
|
+
|
|
53
|
+
# -------------------------------------------------------
|
|
54
|
+
# Parse arguments
|
|
55
|
+
# -------------------------------------------------------
|
|
56
|
+
parse_args() {
|
|
57
|
+
while [[ $# -gt 0 ]]; do
|
|
58
|
+
case "$1" in
|
|
59
|
+
--list)
|
|
60
|
+
MODE="list"
|
|
61
|
+
shift
|
|
62
|
+
;;
|
|
63
|
+
--backup)
|
|
64
|
+
[[ $# -lt 2 ]] && { echo "Error: --backup requires a timestamp value" >&2; usage; }
|
|
65
|
+
MODE="restore"
|
|
66
|
+
BACKUP_TIMESTAMP="$2"
|
|
67
|
+
shift 2
|
|
68
|
+
;;
|
|
69
|
+
--help|-h)
|
|
70
|
+
usage
|
|
71
|
+
;;
|
|
72
|
+
*)
|
|
73
|
+
echo "Error: Unknown argument: $1" >&2
|
|
74
|
+
usage
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
# Default mode: restore from latest
|
|
80
|
+
if [[ -z "$MODE" ]]; then
|
|
81
|
+
MODE="restore"
|
|
82
|
+
fi
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# -------------------------------------------------------
|
|
86
|
+
# List available backup directories, sorted newest-first
|
|
87
|
+
# -------------------------------------------------------
|
|
88
|
+
list_backups() {
|
|
89
|
+
if [[ ! -d "$BACKUP_ROOT" ]] || [[ -z "$(ls -A "$BACKUP_ROOT" 2>/dev/null)" ]]; then
|
|
90
|
+
echo "No backups found in $BACKUP_ROOT"
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
echo "Available backups (newest first):"
|
|
95
|
+
echo ""
|
|
96
|
+
|
|
97
|
+
# List directories sorted newest-first by name (timestamps sort lexicographically)
|
|
98
|
+
local backup_dirs
|
|
99
|
+
backup_dirs=$(find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d | sort -r)
|
|
100
|
+
|
|
101
|
+
if [[ -z "$backup_dirs" ]]; then
|
|
102
|
+
echo "No backups found in $BACKUP_ROOT"
|
|
103
|
+
return 0
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
while IFS= read -r dir; do
|
|
107
|
+
local ts
|
|
108
|
+
ts=$(basename "$dir")
|
|
109
|
+
local file_count
|
|
110
|
+
file_count=$(find "$dir" -type f | wc -l)
|
|
111
|
+
echo " $ts ($file_count files)"
|
|
112
|
+
done <<< "$backup_dirs"
|
|
113
|
+
|
|
114
|
+
echo ""
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# -------------------------------------------------------
|
|
118
|
+
# Require root for restore operations
|
|
119
|
+
# -------------------------------------------------------
|
|
120
|
+
require_root() {
|
|
121
|
+
if [[ "${TUNECTL_SKIP_ROOT_CHECK:-0}" == "1" ]]; then
|
|
122
|
+
return 0
|
|
123
|
+
fi
|
|
124
|
+
if [[ $EUID -ne 0 ]]; then
|
|
125
|
+
echo "Error: Restore requires root privileges. Run with sudo." >&2
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# -------------------------------------------------------
|
|
131
|
+
# Resolve which backup to use
|
|
132
|
+
# Returns the full path to the backup directory
|
|
133
|
+
# -------------------------------------------------------
|
|
134
|
+
resolve_backup_dir() {
|
|
135
|
+
# Check if backup root exists at all
|
|
136
|
+
if [[ ! -d "$BACKUP_ROOT" ]] || [[ -z "$(ls -A "$BACKUP_ROOT" 2>/dev/null)" ]]; then
|
|
137
|
+
echo "Error: No backups found in $BACKUP_ROOT" >&2
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
local backup_dirs
|
|
142
|
+
backup_dirs=$(find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d | sort -r)
|
|
143
|
+
|
|
144
|
+
if [[ -z "$backup_dirs" ]]; then
|
|
145
|
+
echo "Error: No backups found in $BACKUP_ROOT" >&2
|
|
146
|
+
exit 1
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
if [[ -n "$BACKUP_TIMESTAMP" ]]; then
|
|
150
|
+
# Specific backup requested
|
|
151
|
+
local target_dir="${BACKUP_ROOT}/${BACKUP_TIMESTAMP}"
|
|
152
|
+
if [[ ! -d "$target_dir" ]]; then
|
|
153
|
+
echo "Error: Backup '$BACKUP_TIMESTAMP' not found." >&2
|
|
154
|
+
echo "" >&2
|
|
155
|
+
echo "Available backups:" >&2
|
|
156
|
+
while IFS= read -r dir; do
|
|
157
|
+
echo " $(basename "$dir")" >&2
|
|
158
|
+
done <<< "$backup_dirs"
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
echo "$target_dir"
|
|
162
|
+
else
|
|
163
|
+
# Use most recent backup (first in sorted list)
|
|
164
|
+
echo "$backup_dirs" | head -1
|
|
165
|
+
fi
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# -------------------------------------------------------
|
|
169
|
+
# Restore files from a backup directory
|
|
170
|
+
# -------------------------------------------------------
|
|
171
|
+
do_restore() {
|
|
172
|
+
local backup_dir
|
|
173
|
+
backup_dir=$(resolve_backup_dir)
|
|
174
|
+
|
|
175
|
+
local ts
|
|
176
|
+
ts=$(basename "$backup_dir")
|
|
177
|
+
|
|
178
|
+
# Verify backup is not empty
|
|
179
|
+
local file_count
|
|
180
|
+
file_count=$(find "$backup_dir" -type f | wc -l)
|
|
181
|
+
|
|
182
|
+
if [[ $file_count -eq 0 ]]; then
|
|
183
|
+
echo "Error: Backup '$ts' exists but contains no files." >&2
|
|
184
|
+
exit 1
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
local sysroot="${TUNECTL_SYSROOT:-}"
|
|
188
|
+
local restore_failed=false
|
|
189
|
+
|
|
190
|
+
echo "============================================"
|
|
191
|
+
echo " tunectl rollback — Restoring from: $ts"
|
|
192
|
+
echo "============================================"
|
|
193
|
+
echo ""
|
|
194
|
+
echo "Backup contains $file_count file(s)"
|
|
195
|
+
echo ""
|
|
196
|
+
|
|
197
|
+
local restored=0
|
|
198
|
+
local has_sysctl=false
|
|
199
|
+
|
|
200
|
+
# Find all files in the backup directory and restore them
|
|
201
|
+
while IFS= read -r backup_file; do
|
|
202
|
+
# Compute the relative path from the backup dir (this is the original absolute path)
|
|
203
|
+
local rel_path="${backup_file#"$backup_dir"}"
|
|
204
|
+
local dest_path="${sysroot}${rel_path}"
|
|
205
|
+
|
|
206
|
+
# Create destination directory if needed
|
|
207
|
+
mkdir -p "$(dirname "$dest_path")"
|
|
208
|
+
|
|
209
|
+
# Copy the backed-up file to its original path
|
|
210
|
+
cp -p "$backup_file" "$dest_path"
|
|
211
|
+
echo " Restored: $rel_path"
|
|
212
|
+
restored=$((restored + 1))
|
|
213
|
+
|
|
214
|
+
# Track if any sysctl config was restored
|
|
215
|
+
if echo "$rel_path" | grep -q "sysctl"; then
|
|
216
|
+
has_sysctl=true
|
|
217
|
+
fi
|
|
218
|
+
done < <(find "$backup_dir" -type f | sort)
|
|
219
|
+
|
|
220
|
+
echo ""
|
|
221
|
+
|
|
222
|
+
# Reload sysctl if sysctl config files were restored (VAL-ROLL-007)
|
|
223
|
+
if $has_sysctl; then
|
|
224
|
+
if [[ "${TUNECTL_SKIP_SYSCTL_RELOAD:-0}" == "1" ]]; then
|
|
225
|
+
echo "Sysctl reload: skipped (test mode)"
|
|
226
|
+
else
|
|
227
|
+
echo "Reloading sysctl values..."
|
|
228
|
+
if sysctl --system >/dev/null 2>&1; then
|
|
229
|
+
echo " Sysctl values reloaded successfully"
|
|
230
|
+
else
|
|
231
|
+
echo " Error: sysctl --system failed after restore" >&2
|
|
232
|
+
restore_failed=true
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
echo ""
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
echo "============================================"
|
|
239
|
+
echo " Rollback complete — $restored file(s) restored"
|
|
240
|
+
echo " Backup preserved: $backup_dir"
|
|
241
|
+
echo "============================================"
|
|
242
|
+
|
|
243
|
+
if $restore_failed; then
|
|
244
|
+
exit 1
|
|
245
|
+
fi
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# -------------------------------------------------------
|
|
249
|
+
# Main
|
|
250
|
+
# -------------------------------------------------------
|
|
251
|
+
main() {
|
|
252
|
+
parse_args "$@"
|
|
253
|
+
|
|
254
|
+
case "$MODE" in
|
|
255
|
+
list)
|
|
256
|
+
list_backups
|
|
257
|
+
;;
|
|
258
|
+
restore)
|
|
259
|
+
require_root
|
|
260
|
+
do_restore
|
|
261
|
+
;;
|
|
262
|
+
esac
|
|
263
|
+
|
|
264
|
+
exit 0
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
main "$@"
|