v0.11.0 — Phase C: Storage Init Wizard, Data Migration & Startup Fix
- Startup ping: fire heartbeat + health + hub report immediately on boot
(5s delay after scheduler start, instead of waiting 5-15 min for first tick)
- Storage init wizard: new internal/storage/ package with disk scanning
(lsblk -J), format+mount pipeline (sfdisk → mkfs.ext4 → blkid → fstab →
mount → chown), safety guards (system disk detection, confirmation "FORMÁZÁS"),
progress channel, auto-register in settings.json
- Data migration: MigrateAppData() with rsync --info=progress2 progress parsing,
stop/rsync/update-config/start flow, rollback on failure, old data preserved
- New pages: /settings/storage/init (wizard), /stacks/{name}/migrate (migration)
- New API routes: /api/storage/{scan,init,init/status,migrate,migrate/status}
- Deploy page: storage info section for deployed apps (path, size, free, migrate link)
- Settings page: "Mozgatás" button per app in storage path details
- Container: privileged: true, /dev:/dev, /etc/fstab:/host-fstab, /run/udev:/run/udev:ro
- Dockerfile: add util-linux, e2fsprogs, rsync, parted for disk ops
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
//go:build linux
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FormatAndMount formats a disk/partition and mounts it.
|
||||
// Progress updates are sent on the progress channel.
|
||||
// Returns the final mount path on success.
|
||||
func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string, error) {
|
||||
send := func(step, msg string, pct int) {
|
||||
progress <- FormatProgress{Step: step, Message: msg, Percent: pct}
|
||||
}
|
||||
fail := func(step, msg string, err error) error {
|
||||
errStr := ""
|
||||
if err != nil {
|
||||
errStr = err.Error()
|
||||
}
|
||||
progress <- FormatProgress{Step: "error", Message: msg, Error: errStr, Percent: 0}
|
||||
return fmt.Errorf("%s: %w", msg, err)
|
||||
}
|
||||
|
||||
mountPath := "/mnt/" + req.MountName
|
||||
|
||||
// --- Step 1: Validate ---
|
||||
send("validating", "Eszköz ellenőrzése...", 5)
|
||||
|
||||
if err := ValidateMountName(req.MountName); err != nil {
|
||||
return "", fail("validating", "Érvénytelen csatlakoztatási név", err)
|
||||
}
|
||||
if _, err := os.Stat(req.DevicePath); err != nil {
|
||||
return "", fail("validating", "Az eszköz nem létezik: "+req.DevicePath, err)
|
||||
}
|
||||
|
||||
isSystem, err := IsSystemDisk(req.DevicePath)
|
||||
if err != nil {
|
||||
return "", fail("validating", "Rendszermeghajtó ellenőrzése sikertelen", err)
|
||||
}
|
||||
if isSystem {
|
||||
return "", fail("validating", "Ez a rendszermeghajtó — nem formázható!", fmt.Errorf("device is system disk"))
|
||||
}
|
||||
|
||||
mounted, err := IsDeviceMounted(req.DevicePath)
|
||||
if err != nil {
|
||||
return "", fail("validating", "Csatlakoztatási állapot ellenőrzése sikertelen", err)
|
||||
}
|
||||
if mounted {
|
||||
return "", fail("validating", "Az eszköz már csatlakoztatva van", fmt.Errorf("device already mounted"))
|
||||
}
|
||||
|
||||
inUse, err := IsMountPathInUse(mountPath)
|
||||
if err != nil {
|
||||
return "", fail("validating", "Csatlakoztatási útvonal ellenőrzése sikertelen", err)
|
||||
}
|
||||
if inUse {
|
||||
return "", fail("validating", "A csatlakoztatási útvonal már használatban van: "+mountPath, fmt.Errorf("mount path in use"))
|
||||
}
|
||||
|
||||
send("validating", "Ellenőrzés kész", 10)
|
||||
|
||||
// --- Step 2: Partition (if requested) ---
|
||||
partDev := req.DevicePath
|
||||
if req.CreatePartition {
|
||||
send("partitioning", "Partíció létrehozása...", 15)
|
||||
|
||||
sfdiskInput := "label: gpt\n,,,L\n"
|
||||
cmd := exec.Command("sfdisk", req.DevicePath)
|
||||
cmd.Stdin = strings.NewReader(sfdiskInput)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return "", fail("partitioning", "Partícionálás sikertelen: "+string(out), err)
|
||||
}
|
||||
|
||||
_ = exec.Command("partprobe", req.DevicePath).Run()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
partDev = req.DevicePath + "1"
|
||||
if strings.Contains(req.DevicePath, "nvme") {
|
||||
partDev = req.DevicePath + "p1"
|
||||
}
|
||||
if _, err := os.Stat(partDev); err != nil {
|
||||
return "", fail("partitioning", "Partíció nem található a létrehozás után: "+partDev, err)
|
||||
}
|
||||
|
||||
send("partitioning", "Partíció létrehozva: "+partDev, 25)
|
||||
}
|
||||
|
||||
// --- Step 3: Format ---
|
||||
send("formatting", "Fájlrendszer formázása (ext4)...", 30)
|
||||
|
||||
label := req.Label
|
||||
if label == "" {
|
||||
label = req.MountName
|
||||
}
|
||||
if len(label) > 16 {
|
||||
label = label[:16]
|
||||
}
|
||||
|
||||
mkfsCmd := exec.Command("mkfs.ext4", "-L", label, "-F", partDev)
|
||||
var mkfsOut bytes.Buffer
|
||||
mkfsCmd.Stdout = &mkfsOut
|
||||
mkfsCmd.Stderr = &mkfsOut
|
||||
if err := mkfsCmd.Run(); err != nil {
|
||||
return "", fail("formatting", "Formázás sikertelen: "+mkfsOut.String(), err)
|
||||
}
|
||||
|
||||
send("formatting", "Formázás kész", 60)
|
||||
|
||||
// --- Step 4: Mount ---
|
||||
send("mounting", "Csatlakoztatás: "+mountPath+"...", 65)
|
||||
|
||||
if err := os.MkdirAll(mountPath, 0755); err != nil {
|
||||
return "", fail("mounting", "Csatlakoztatási mappa nem hozható létre: "+mountPath, err)
|
||||
}
|
||||
|
||||
uuidOut, err := exec.Command("blkid", "-s", "UUID", "-o", "value", partDev).Output()
|
||||
if err != nil {
|
||||
return "", fail("mounting", "UUID lekérése sikertelen", err)
|
||||
}
|
||||
uuid := strings.TrimSpace(string(uuidOut))
|
||||
if uuid == "" {
|
||||
return "", fail("mounting", "UUID üres a formázás után", fmt.Errorf("empty UUID"))
|
||||
}
|
||||
|
||||
// Backup fstab (non-fatal)
|
||||
_ = BackupFstab(FstabPath)
|
||||
|
||||
if err := AppendFstabEntry(FstabPath, uuid, mountPath, "ext4", "defaults,nofail,noatime"); err != nil {
|
||||
return "", fail("mounting", "fstab bejegyzés hozzáadása sikertelen", err)
|
||||
}
|
||||
|
||||
if out, err := exec.Command("mount", mountPath).CombinedOutput(); err != nil {
|
||||
return "", fail("mounting", "Csatlakoztatás sikertelen: "+string(out), err)
|
||||
}
|
||||
|
||||
send("mounting", "Csatlakoztatva: "+mountPath, 80)
|
||||
|
||||
// --- Step 5: Permissions + subdirs ---
|
||||
send("permissions", "Mappák létrehozása és jogosultságok beállítása...", 85)
|
||||
|
||||
_ = exec.Command("chown", "1000:1000", mountPath).Run()
|
||||
|
||||
for _, subdir := range []string{"storage", "Dokumentumok"} {
|
||||
dir := filepath.Join(mountPath, subdir)
|
||||
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||
_ = exec.Command("chown", "1000:1000", dir).Run()
|
||||
}
|
||||
}
|
||||
|
||||
send("done", "Meghajtó sikeresen inicializálva: "+mountPath, 100)
|
||||
|
||||
return mountPath, nil
|
||||
}
|
||||
|
||||
// GetDeviceUUID returns the UUID of a block device/partition.
|
||||
func GetDeviceUUID(devicePath string) (string, error) {
|
||||
out, err := exec.Command("blkid", "-s", "UUID", "-o", "value", devicePath).Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
// ReadFstab reads the current fstab content.
|
||||
func ReadFstab() (string, error) {
|
||||
data, err := os.ReadFile(FstabPath)
|
||||
if err != nil {
|
||||
data, err = os.ReadFile("/etc/fstab")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
Reference in New Issue
Block a user