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.
@@ -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 "$@"