Merge pull request #261 from naturallaw777/copilot/check-timechain-data-drive

Preserve existing Bitcoin timechain data drive during OS reinstall
This commit is contained in:
Sovran_Systems
2026-04-15 15:25:05 -05:00
committed by GitHub
2 changed files with 110 additions and 15 deletions
+79 -8
View File
@@ -8,6 +8,7 @@ import os
import secrets import secrets
import subprocess import subprocess
import sys import sys
import tempfile
import threading import threading
import time import time
@@ -156,6 +157,7 @@ class InstallerWindow(Adw.ApplicationWindow):
self.boot_size = None self.boot_size = None
self.data_disk = None self.data_disk = None
self.data_size = None self.data_size = None
self.data_drive_has_timechain = False
self.free_password = None self.free_password = None
# Root navigation view # Root navigation view
@@ -667,10 +669,18 @@ class InstallerWindow(Adw.ApplicationWindow):
def push_disk_confirm(self): def push_disk_confirm(self):
"""Show the selected drives and ask the user to type ERASE to confirm.""" """Show the selected drives and ask the user to type ERASE to confirm."""
self.data_drive_has_timechain = False
if self.data_disk:
data_path = f"/dev/{self.data_disk}"
self.data_drive_has_timechain = self.detect_existing_timechain_data(data_path)
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
# Disk info group # Disk info group
disk_group = Adw.PreferencesGroup() disk_group = Adw.PreferencesGroup()
if self.data_disk and self.data_drive_has_timechain:
disk_group.set_title("OS drive to be erased (data drive preserved)")
else:
disk_group.set_title("Drives to be erased") disk_group.set_title("Drives to be erased")
disk_group.set_margin_top(24) disk_group.set_margin_top(24)
disk_group.set_margin_start(40) disk_group.set_margin_start(40)
@@ -688,11 +698,20 @@ class InstallerWindow(Adw.ApplicationWindow):
data_row.set_subtitle(f"/dev/{self.data_disk}{human_size(self.data_size)}") data_row.set_subtitle(f"/dev/{self.data_disk}{human_size(self.data_size)}")
data_row.add_prefix(symbolic_icon("drive-harddisk-symbolic")) data_row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
disk_group.add(data_row) disk_group.add(data_row)
if self.data_drive_has_timechain:
note_row = Adw.ActionRow()
note_row.set_title(f"Existing Bitcoin timechain detected on /dev/{self.data_disk}")
note_row.set_subtitle("Data will be preserved and mounted as-is.")
note_row.add_prefix(symbolic_icon("emblem-ok-symbolic"))
disk_group.add(note_row)
outer.append(disk_group) outer.append(disk_group)
# Warning banner # Warning banner
banner = Adw.Banner() banner = Adw.Banner()
if self.data_disk and self.data_drive_has_timechain:
banner.set_title("⚠ All data on the OS disk will be permanently destroyed. Existing Bitcoin data disk will be preserved.")
else:
banner.set_title("⚠ All data on the above disk(s) will be permanently destroyed.") banner.set_title("⚠ All data on the above disk(s) will be permanently destroyed.")
banner.set_revealed(True) banner.set_revealed(True)
banner.set_margin_top(16) banner.set_margin_top(16)
@@ -775,9 +794,61 @@ class InstallerWindow(Adw.ApplicationWindow):
# ── Worker: partition ───────────────────────────────────────────────── # ── Worker: partition ─────────────────────────────────────────────────
def partition_path(self, dev_path, num):
return f"{dev_path}p{num}" if "nvme" in dev_path else f"{dev_path}{num}"
def detect_existing_timechain_data(self, data_path, buf=None):
data_p1 = self.partition_path(data_path, 1)
if not os.path.exists(data_p1):
return False
label = ""
for cmd in (
["sudo", "lsblk", "-no", "LABEL", data_p1],
["sudo", "blkid", "-o", "value", "-s", "LABEL", data_p1],
):
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode == 0:
stdout = proc.stdout.strip()
label = stdout.splitlines()[0] if stdout else ""
if label:
break
if label != "BTCEcoandBackup":
return False
check_mount = tempfile.mkdtemp(prefix="sovran-installer-data-check-")
mounted = False
try:
run(["sudo", "mount", "-o", "ro", data_p1, check_mount])
mounted = True
has_bitcoin = os.path.isdir(f"{check_mount}/BTCEcoandBackup/Bitcoin_Node")
has_electrs = os.path.isdir(f"{check_mount}/BTCEcoandBackup/Electrs_Data")
if has_bitcoin and has_electrs:
if buf is not None:
GLib.idle_add(
append_text,
buf,
"=== Existing Bitcoin timechain detected on data drive — preserving data ===\n",
)
return True
return False
except Exception as e:
log(f"Timechain detection failed for {data_p1} at mount/check step ({check_mount}): {e}")
return False
finally:
if mounted:
subprocess.run(["sudo", "umount", check_mount], capture_output=True, text=True)
subprocess.run(["sudo", "rmdir", check_mount], capture_output=True, text=True)
def do_partition(self, buf): def do_partition(self, buf):
boot_path = f"/dev/{self.boot_disk}" boot_path = f"/dev/{self.boot_disk}"
data_path = f"/dev/{self.data_disk}" if self.data_disk else None data_path = f"/dev/{self.data_disk}" if self.data_disk else None
self.data_drive_has_timechain = False
if data_path:
self.data_drive_has_timechain = self.detect_existing_timechain_data(data_path, buf)
# ── Wipe disk(s) ── # ── Wipe disk(s) ──
GLib.idle_add(append_text, buf, "=== Wiping disk(s) ===\n") GLib.idle_add(append_text, buf, "=== Wiping disk(s) ===\n")
@@ -785,12 +856,12 @@ class InstallerWindow(Adw.ApplicationWindow):
run_stream(["sudo", "sgdisk", "--zap-all", boot_path], buf) run_stream(["sudo", "sgdisk", "--zap-all", boot_path], buf)
run_stream(["sudo", "wipefs", "--all", "--force", boot_path], buf) run_stream(["sudo", "wipefs", "--all", "--force", boot_path], buf)
if data_path: if data_path and not self.data_drive_has_timechain:
run_stream(["sudo", "sgdisk", "--zap-all", data_path], buf) run_stream(["sudo", "sgdisk", "--zap-all", data_path], buf)
run_stream(["sudo", "wipefs", "--all", "--force", data_path], buf) run_stream(["sudo", "wipefs", "--all", "--force", data_path], buf)
run_stream(["sudo", "partprobe", boot_path], buf) run_stream(["sudo", "partprobe", boot_path], buf)
if data_path: if data_path and not self.data_drive_has_timechain:
run_stream(["sudo", "partprobe", data_path], buf) run_stream(["sudo", "partprobe", data_path], buf)
time.sleep(2) time.sleep(2)
@@ -806,7 +877,7 @@ class InstallerWindow(Adw.ApplicationWindow):
time.sleep(2) time.sleep(2)
# ── Partition data disk (if selected) ── # ── Partition data disk (if selected) ──
if data_path: if data_path and not self.data_drive_has_timechain:
GLib.idle_add(append_text, buf, "\n=== Partitioning data disk ===\n") GLib.idle_add(append_text, buf, "\n=== Partitioning data disk ===\n")
run_stream(["sudo", "sgdisk", run_stream(["sudo", "sgdisk",
"-n", "1:1M:0", "-t", "1:8300", "-c", "1:primary", "-n", "1:1M:0", "-t", "1:8300", "-c", "1:primary",
@@ -817,14 +888,14 @@ class InstallerWindow(Adw.ApplicationWindow):
# ── Format partitions ── # ── Format partitions ──
GLib.idle_add(append_text, buf, "\n=== Formatting partitions ===\n") GLib.idle_add(append_text, buf, "\n=== Formatting partitions ===\n")
boot_p1 = f"{boot_path}p1" if "nvme" in boot_path else f"{boot_path}1" boot_p1 = self.partition_path(boot_path, 1)
boot_p2 = f"{boot_path}p2" if "nvme" in boot_path else f"{boot_path}2" boot_p2 = self.partition_path(boot_path, 2)
run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf) run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf)
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "sovran_systemsos", boot_p2], buf) run_stream(["sudo", "mkfs.ext4", "-F", "-L", "sovran_systemsos", boot_p2], buf)
if data_path: if data_path and not self.data_drive_has_timechain:
data_p1 = f"{data_path}p1" if "nvme" in data_path else f"{data_path}1" data_p1 = self.partition_path(data_path, 1)
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "BTCEcoandBackup", data_p1], buf) run_stream(["sudo", "mkfs.ext4", "-F", "-L", "BTCEcoandBackup", data_p1], buf)
# ── Mount filesystems ── # ── Mount filesystems ──
@@ -834,7 +905,7 @@ class InstallerWindow(Adw.ApplicationWindow):
run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf) run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf)
if data_path: if data_path:
data_p1 = f"{data_path}p1" if "nvme" in data_path else f"{data_path}1" data_p1 = self.partition_path(data_path, 1)
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive"], buf) run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive"], buf)
run_stream(["sudo", "mount", data_p1, "/mnt/run/media/Second_Drive"], buf) run_stream(["sudo", "mount", data_p1, "/mnt/run/media/Second_Drive"], buf)
run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"], buf) run_stream(["sudo", "mkdir", "-p", "/mnt/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"], buf)
+28 -4
View File
@@ -24,6 +24,7 @@ ROLE="server"
DEPLOY_KEY="" DEPLOY_KEY=""
HEADSCALE_SERVER="" HEADSCALE_SERVER=""
HEADSCALE_KEY="" HEADSCALE_KEY=""
DATA_DISK_HAS_TIMECHAIN=false
FLAKE="/etc/sovran/flake" FLAKE="/etc/sovran/flake"
LOG="/tmp/sovran-headless-install.log" LOG="/tmp/sovran-headless-install.log"
@@ -102,19 +103,42 @@ part_suffix() {
fi fi
} }
# ── Detect existing Bitcoin timechain data on data disk ───────────────────────
if [[ -n "$DATA_DISK" ]]; then
DATA_P1=$(part_suffix "$DATA_DISK" 1)
if [[ -b "$DATA_P1" ]]; then
DATA_LABEL=$(lsblk -no LABEL "$DATA_P1" 2>/dev/null | head -n1 || true)
if [[ -z "$DATA_LABEL" ]]; then
DATA_LABEL=$(blkid -o value -s LABEL "$DATA_P1" 2>/dev/null || true)
fi
if [[ "$DATA_LABEL" == "BTCEcoandBackup" ]]; then
CHECK_MOUNT=$(mktemp -d /tmp/sovran-data-check.XXXXXX)
if mount -o ro "$DATA_P1" "$CHECK_MOUNT" 2>/dev/null; then
if [[ -d "$CHECK_MOUNT/BTCEcoandBackup/Bitcoin_Node" && -d "$CHECK_MOUNT/BTCEcoandBackup/Electrs_Data" ]]; then
DATA_DISK_HAS_TIMECHAIN=true
log "Existing Bitcoin timechain detected on data drive — preserving data"
fi
umount "$CHECK_MOUNT" || true
fi
rmdir "$CHECK_MOUNT" 2>/dev/null || true
fi
fi
fi
# ── Step 1: Wipe disks ──────────────────────────────────────────────────────── # ── Step 1: Wipe disks ────────────────────────────────────────────────────────
log "=== Wiping disk(s) ===" log "=== Wiping disk(s) ==="
sgdisk --zap-all "$DISK" sgdisk --zap-all "$DISK"
wipefs --all --force "$DISK" wipefs --all --force "$DISK"
if [[ -n "$DATA_DISK" ]]; then if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
sgdisk --zap-all "$DATA_DISK" sgdisk --zap-all "$DATA_DISK"
wipefs --all --force "$DATA_DISK" wipefs --all --force "$DATA_DISK"
fi fi
partprobe "$DISK" partprobe "$DISK"
[[ -n "$DATA_DISK" ]] && partprobe "$DATA_DISK" [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]] && partprobe "$DATA_DISK"
sleep 2 sleep 2
# ── Step 2: Partition OS disk ───────────────────────────────────────────────── # ── Step 2: Partition OS disk ─────────────────────────────────────────────────
@@ -129,7 +153,7 @@ partprobe "$DISK"
sleep 2 sleep 2
# ── Step 3: Partition data disk (if present) ────────────────────────────────── # ── Step 3: Partition data disk (if present) ──────────────────────────────────
if [[ -n "$DATA_DISK" ]]; then if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
log "=== Partitioning data disk ===" log "=== Partitioning data disk ==="
sgdisk \ sgdisk \
-n "1:1M:0" -t "1:8300" -c "1:primary" \ -n "1:1M:0" -t "1:8300" -c "1:primary" \
@@ -147,7 +171,7 @@ BOOT_P2=$(part_suffix "$DISK" 2)
mkfs.vfat -F 32 "$BOOT_P1" mkfs.vfat -F 32 "$BOOT_P1"
mkfs.ext4 -F -L sovran_systemsos "$BOOT_P2" mkfs.ext4 -F -L sovran_systemsos "$BOOT_P2"
if [[ -n "$DATA_DISK" ]]; then if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
DATA_P1=$(part_suffix "$DATA_DISK" 1) DATA_P1=$(part_suffix "$DATA_DISK" 1)
mkfs.ext4 -F -L BTCEcoandBackup "$DATA_P1" mkfs.ext4 -F -L BTCEcoandBackup "$DATA_P1"
fi fi