From 05151fbaef912661eb58151f90a1108a4d8c31d4 Mon Sep 17 00:00:00 2001 From: hayzamjs Date: Thu, 21 May 2026 14:38:27 +0530 Subject: [PATCH] zfs: enc/restore: deal with children w/different keys --- go.mod | 2 +- go.sum | 4 +- internal/services/zelta/encryption.go | 10 ++- internal/services/zelta/restore.go | 96 ++++++++++++++++++++------- web/src/locales/cs.po | 9 ++- web/src/locales/de.po | 9 ++- web/src/locales/en.po | 13 ++-- web/src/locales/hi.po | 9 ++- web/src/locales/mal.po | 9 ++- web/src/locales/zh-CN.po | 9 ++- 10 files changed, 104 insertions(+), 66 deletions(-) diff --git a/go.mod b/go.mod index a00a189e..ce51abdc 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ ignore assets ignore internal/testutil require ( - github.com/alchemillahq/gzfs v0.0.0-20260521021508-f16dc9b17ab5 + github.com/alchemillahq/gzfs v0.0.0-20260521085926-98d1cc21f2e9 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/beevik/etree v1.6.0 github.com/cavaliergopher/grab/v3 v3.0.1 diff --git a/go.sum b/go.sum index 95a5ed05..3ef1e2e1 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,10 @@ github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Sereal/Sereal/Go/sereal v0.0.0-20231009093132-b9187f1a92c6/go.mod h1:JwrycNnC8+sZPDyzM3MQ86LvaGzSpfxg885KOOwFRW4= -github.com/alchemillahq/gzfs v0.0.0-20260409031910-c82074def96e h1:BCzTeVfyAsH7W4dp7s+rmxI8C+fcMZWtQxat5fiaFl0= -github.com/alchemillahq/gzfs v0.0.0-20260409031910-c82074def96e/go.mod h1:uml1U7jMNb/dV6NS3nl4QqW6Nv/CBUDdEdzhl0S/p4Y= github.com/alchemillahq/gzfs v0.0.0-20260521021508-f16dc9b17ab5 h1:eCJ+N/3yihBNrPkq2MfRiG72IZUc7qVp0Wf2enNMqjM= github.com/alchemillahq/gzfs v0.0.0-20260521021508-f16dc9b17ab5/go.mod h1:uml1U7jMNb/dV6NS3nl4QqW6Nv/CBUDdEdzhl0S/p4Y= +github.com/alchemillahq/gzfs v0.0.0-20260521085926-98d1cc21f2e9 h1:hRoVcJxJx+rmSOGGv8yk43X+xYqJHbMy4oQrvH6x19M= +github.com/alchemillahq/gzfs v0.0.0-20260521085926-98d1cc21f2e9/go.mod h1:uml1U7jMNb/dV6NS3nl4QqW6Nv/CBUDdEdzhl0S/p4Y= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= diff --git a/internal/services/zelta/encryption.go b/internal/services/zelta/encryption.go index 7b006190..5e091267 100644 --- a/internal/services/zelta/encryption.go +++ b/internal/services/zelta/encryption.go @@ -137,7 +137,7 @@ func (s *Service) ensureEncryptionKeyForDataset(ctx context.Context, ds *gzfs.Da if err := s.EnsureEncryptionKeyFile(uuid); err != nil { return false, fmt.Errorf("encryption_key_not_found_in_cluster_store: %s", uuid) } - if err := ds.LoadKey(ctx, false); err != nil { + if err := ds.LoadKey(ctx, true); err != nil { return false, fmt.Errorf("load_key_failed: %w", err) } return true, nil @@ -145,21 +145,19 @@ func (s *Service) ensureEncryptionKeyForDataset(ctx context.Context, ds *gzfs.Da // keylocation is "prompt" — the original key file wasn't available on // the server that received this dataset (e.g. a backup target). Try each - // key in the cluster store; match by attempting to load. + // key in the cluster store until one loads successfully. keys, listErr := s.Cluster.ListEncryptionKeys() if listErr != nil { logger.L.Warn().Err(listErr).Str("dataset", ds.Name). - Msg("encrypted_dataset_cannot_list_keys_for_prompt_keylocation") + Msg("prompt_keylocation_cannot_list_keys") return false, nil } for _, key := range keys { - if err := ds.LoadKeyWithPassphrase(ctx, key.KeyData); err != nil { + if err := ds.LoadKeyWithPassphrase(ctx, key.KeyData, true); err != nil { continue } - // Key loaded — materialize the key file and fix keylocation - // so it auto-loads on next boot. if writeErr := s.EnsureEncryptionKeyFile(key.UUID); writeErr != nil { logger.L.Warn().Err(writeErr).Str("uuid", key.UUID). Str("dataset", ds.Name).Msg("prompt_key_loaded_but_file_write_failed") diff --git a/internal/services/zelta/restore.go b/internal/services/zelta/restore.go index 6603ef80..dd8f7ba0 100644 --- a/internal/services/zelta/restore.go +++ b/internal/services/zelta/restore.go @@ -12,6 +12,7 @@ import ( "context" "fmt" "runtime/debug" + "sort" "strings" "time" @@ -345,43 +346,88 @@ func (s *Service) runRestoreVMJob( // fixRestoredProperties corrects ZFS properties after a restore so the dataset // behaves like the original (not readonly, correct mountpoint, canmount=on). // For encrypted datasets, it ensures the key file is present and loads the key -// before mounting. +// before mounting. Walks all child datasets under the root to handle +// independent encryption roots. func (s *Service) fixRestoredProperties(ctx context.Context, dataset string) { - ds, err := s.getLocalDataset(ctx, dataset) + dataset = normalizeDatasetPath(dataset) + if dataset == "" { + return + } + + allDatasets, err := s.listLocalFilesystemDatasets(ctx) if err != nil { - logger.L.Warn().Err(err).Str("dataset", dataset).Msg("fix_restored_property_failed") - return - } - if ds == nil { - logger.L.Warn().Str("dataset", dataset).Msg("fix_restored_property_failed_dataset_not_found") + logger.L.Warn().Err(err).Msg("fix_restored_property_list_datasets_failed") return } - if ds.IsEncrypted() { - keyLoaded, err := s.ensureEncryptionKeyForDataset(ctx, ds) + seen := map[string]struct{}{dataset: {}} + subtree := []string{dataset} + prefix := dataset + "/" + + for _, candidate := range allDatasets { + ds := normalizeDatasetPath(candidate) + if ds == "" || ds == dataset { + continue + } + if !strings.HasPrefix(ds, prefix) { + continue + } + if _, ok := seen[ds]; ok { + continue + } + seen[ds] = struct{}{} + subtree = append(subtree, ds) + } + + sort.SliceStable(subtree, func(i, j int) bool { + di := strings.Count(subtree[i], "/") + dj := strings.Count(subtree[j], "/") + if di == dj { + return subtree[i] < subtree[j] + } + return di < dj + }) + + for idx, dsName := range subtree { + ds, err := s.getLocalDataset(ctx, dsName) if err != nil { - logger.L.Error().Err(err).Str("dataset", dataset).Msg("fix_restored_property_key_load_failed") - return + logger.L.Warn().Err(err).Str("dataset", dsName).Msg("fix_restored_property_get_failed") + continue } - if !keyLoaded { - logger.L.Warn().Str("dataset", dataset).Msg("fix_restored_property_key_not_auto_loaded") + if ds == nil { + logger.L.Warn().Str("dataset", dsName).Msg("fix_restored_property_dataset_not_found") + continue } - } - if err := ds.SetProperties(ctx, "readonly", "off", "canmount", "on"); err != nil { - logger.L.Warn().Err(err).Str("dataset", dataset).Msg("fix_restored_property_set_failed") - } + if ds.IsEncrypted() { + keyLoaded, err := s.ensureEncryptionKeyForDataset(ctx, ds) + if err != nil { + logger.L.Error().Err(err).Str("dataset", dsName).Msg("fix_restored_property_key_load_failed") + continue + } + if !keyLoaded { + logger.L.Warn().Str("dataset", dsName).Msg("fix_restored_property_key_not_auto_loaded") + continue + } + } - if _, err := utils.RunCommandWithContext(ctx, "zfs", "inherit", "mountpoint", dataset); err != nil { - logger.L.Warn().Err(err).Str("dataset", dataset).Msg("fix_restored_property_inherit_mountpoint_failed") - } + if err := ds.SetProperties(ctx, "readonly", "off", "canmount", "on"); err != nil { + logger.L.Warn().Err(err).Str("dataset", dsName).Msg("fix_restored_property_set_failed") + } - if err := ds.Mount(ctx, false); err != nil { - lowerErr := strings.ToLower(strings.TrimSpace(err.Error())) - if strings.Contains(lowerErr, "already mounted") { - return + if idx == 0 { + if _, err := utils.RunCommandWithContext(ctx, "zfs", "inherit", "mountpoint", dsName); err != nil { + logger.L.Warn().Err(err).Str("dataset", dsName).Msg("fix_restored_property_inherit_mountpoint_failed") + } + } + + if err := ds.Mount(ctx, false); err != nil { + lowerErr := strings.ToLower(strings.TrimSpace(err.Error())) + if strings.Contains(lowerErr, "already mounted") { + continue + } + logger.L.Warn().Err(err).Str("dataset", dsName).Msg("fix_restored_property_mount_failed") } - logger.L.Warn().Err(err).Str("dataset", dataset).Msg("fix_restored_property_mount_failed") } } diff --git a/web/src/locales/cs.po b/web/src/locales/cs.po index 8e7da6dd..73855801 100644 --- a/web/src/locales/cs.po +++ b/web/src/locales/cs.po @@ -13249,11 +13249,10 @@ msgstr "" msgid "Redirecting to Traffic Rules..." msgstr "" -#: src/routes/inactive-node/+page.svelte -msgid "" -"This node can’t be reached right now. Check the network connection and confirm the node is\n" -"powered on." -msgstr "Tento uzel nyní není dosažitelný. Zkontrolujte připojení k síti a to, zda je uzel zapnutý." +#~ msgid "" +#~ "This node can’t be reached right now. Check the network connection and confirm the node is\n" +#~ "powered on." +#~ msgstr "Tento uzel nyní není dosažitelný. Zkontrolujte připojení k síti a to, zda je uzel zapnutý." #~ msgid "⌘⇧Q" #~ msgstr "⌘⇧Q" diff --git a/web/src/locales/de.po b/web/src/locales/de.po index ca6eebb9..e72de88d 100644 --- a/web/src/locales/de.po +++ b/web/src/locales/de.po @@ -13246,11 +13246,10 @@ msgstr "" msgid "Redirecting to Traffic Rules..." msgstr "" -#: src/routes/inactive-node/+page.svelte -msgid "" -"This node can’t be reached right now. Check the network connection and confirm the node is\n" -"powered on." -msgstr "" +#~ msgid "" +#~ "This node can’t be reached right now. Check the network connection and confirm the node is\n" +#~ "powered on." +#~ msgstr "" #~ msgid "⌘⇧Q" #~ msgstr "" diff --git a/web/src/locales/en.po b/web/src/locales/en.po index 65d150c9..006a59fa 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -13246,13 +13246,12 @@ msgstr "Jail Template - {0}" msgid "Redirecting to Traffic Rules..." msgstr "Redirecting to Traffic Rules..." -#: src/routes/inactive-node/+page.svelte -msgid "" -"This node can’t be reached right now. Check the network connection and confirm the node is\n" -"powered on." -msgstr "" -"This node can’t be reached right now. Check the network connection and confirm the node is\n" -"powered on." +#~ msgid "" +#~ "This node can’t be reached right now. Check the network connection and confirm the node is\n" +#~ "powered on." +#~ msgstr "" +#~ "This node can’t be reached right now. Check the network connection and confirm the node is\n" +#~ "powered on." #~ msgid "⌘⇧Q" #~ msgstr "⌘⇧Q" diff --git a/web/src/locales/hi.po b/web/src/locales/hi.po index f2ca367b..3bcce536 100644 --- a/web/src/locales/hi.po +++ b/web/src/locales/hi.po @@ -13246,11 +13246,10 @@ msgstr "" msgid "Redirecting to Traffic Rules..." msgstr "" -#: src/routes/inactive-node/+page.svelte -msgid "" -"This node can’t be reached right now. Check the network connection and confirm the node is\n" -"powered on." -msgstr "" +#~ msgid "" +#~ "This node can’t be reached right now. Check the network connection and confirm the node is\n" +#~ "powered on." +#~ msgstr "" #~ msgid "⌘⇧Q" #~ msgstr "" diff --git a/web/src/locales/mal.po b/web/src/locales/mal.po index f57e00c0..ed338e0a 100644 --- a/web/src/locales/mal.po +++ b/web/src/locales/mal.po @@ -13246,11 +13246,10 @@ msgstr "" msgid "Redirecting to Traffic Rules..." msgstr "" -#: src/routes/inactive-node/+page.svelte -msgid "" -"This node can’t be reached right now. Check the network connection and confirm the node is\n" -"powered on." -msgstr "" +#~ msgid "" +#~ "This node can’t be reached right now. Check the network connection and confirm the node is\n" +#~ "powered on." +#~ msgstr "" #~ msgid "⌘⇧Q" #~ msgstr "" diff --git a/web/src/locales/zh-CN.po b/web/src/locales/zh-CN.po index 3486afb9..7eadaa17 100644 --- a/web/src/locales/zh-CN.po +++ b/web/src/locales/zh-CN.po @@ -13243,11 +13243,10 @@ msgstr "Jail 模板 - {0}" msgid "Redirecting to Traffic Rules..." msgstr "正在重定向到流量规则..." -#: src/routes/inactive-node/+page.svelte -msgid "" -"This node can’t be reached right now. Check the network connection and confirm the node is\n" -"powered on." -msgstr "目前无法连接到此节点。请检查网络连接并确认节点已开启电源。" +#~ msgid "" +#~ "This node can’t be reached right now. Check the network connection and confirm the node is\n" +#~ "powered on." +#~ msgstr "目前无法连接到此节点。请检查网络连接并确认节点已开启电源。" #~ msgid "⌘⇧Q" #~ msgstr ""