zfs: enc/restore: deal with children w/different keys

This commit is contained in:
hayzamjs
2026-05-21 14:38:27 +05:30
parent 9a1b590aae
commit 05151fbaef
10 changed files with 104 additions and 66 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+4 -6
View File
@@ -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")
+71 -25
View File
@@ -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")
}
}
+4 -5
View File
@@ -13249,11 +13249,10 @@ msgstr ""
msgid "Redirecting to Traffic Rules..."
msgstr ""
#: src/routes/inactive-node/+page.svelte
msgid ""
"This node cant 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 cant 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"
+4 -5
View File
@@ -13246,11 +13246,10 @@ msgstr ""
msgid "Redirecting to Traffic Rules..."
msgstr ""
#: src/routes/inactive-node/+page.svelte
msgid ""
"This node cant be reached right now. Check the network connection and confirm the node is\n"
"powered on."
msgstr ""
#~ msgid ""
#~ "This node cant be reached right now. Check the network connection and confirm the node is\n"
#~ "powered on."
#~ msgstr ""
#~ msgid "⌘⇧Q"
#~ msgstr ""
+6 -7
View File
@@ -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 cant be reached right now. Check the network connection and confirm the node is\n"
"powered on."
msgstr ""
"This node cant be reached right now. Check the network connection and confirm the node is\n"
"powered on."
#~ msgid ""
#~ "This node cant be reached right now. Check the network connection and confirm the node is\n"
#~ "powered on."
#~ msgstr ""
#~ "This node cant be reached right now. Check the network connection and confirm the node is\n"
#~ "powered on."
#~ msgid "⌘⇧Q"
#~ msgstr "⌘⇧Q"
+4 -5
View File
@@ -13246,11 +13246,10 @@ msgstr ""
msgid "Redirecting to Traffic Rules..."
msgstr ""
#: src/routes/inactive-node/+page.svelte
msgid ""
"This node cant be reached right now. Check the network connection and confirm the node is\n"
"powered on."
msgstr ""
#~ msgid ""
#~ "This node cant be reached right now. Check the network connection and confirm the node is\n"
#~ "powered on."
#~ msgstr ""
#~ msgid "⌘⇧Q"
#~ msgstr ""
+4 -5
View File
@@ -13246,11 +13246,10 @@ msgstr ""
msgid "Redirecting to Traffic Rules..."
msgstr ""
#: src/routes/inactive-node/+page.svelte
msgid ""
"This node cant be reached right now. Check the network connection and confirm the node is\n"
"powered on."
msgstr ""
#~ msgid ""
#~ "This node cant be reached right now. Check the network connection and confirm the node is\n"
#~ "powered on."
#~ msgstr ""
#~ msgid "⌘⇧Q"
#~ msgstr ""
+4 -5
View File
@@ -13243,11 +13243,10 @@ msgstr "Jail 模板 - {0}"
msgid "Redirecting to Traffic Rules..."
msgstr "正在重定向到流量规则..."
#: src/routes/inactive-node/+page.svelte
msgid ""
"This node cant be reached right now. Check the network connection and confirm the node is\n"
"powered on."
msgstr "目前无法连接到此节点。请检查网络连接并确认节点已开启电源。"
#~ msgid ""
#~ "This node cant be reached right now. Check the network connection and confirm the node is\n"
#~ "powered on."
#~ msgstr "目前无法连接到此节点。请检查网络连接并确认节点已开启电源。"
#~ msgid "⌘⇧Q"
#~ msgstr ""