Repository navigation
Add: Allow reusing existing local storage pools - #1641
krishvsoni wants to merge 11 commits into
Conversation
The answer to "Select which disks to wipe" was overwritten afterwards, so every selected local storage disk was always wiped. Signed-off-by: krish <krish@lyzr.com>
Disks with partitions were dropped entirely, which hides disks holding an existing local storage pool. Keep the unmounted ones separately as ReusableDisks and add IsPristineDisk which treats a disk as pristine only if it has neither a filesystem UUID nor partitions. Signed-off-by: krish <krish@lyzr.com>
Signed-off-by: krish <krish@lyzr.com>
…g systems When adding systems and LXD supports storage_source_recover, offer reusable disks for local storage and ask whether to reuse the existing storage pool on any selected non-pristine disk. Reused disks are set up with source.recover and are never wiped or offered for remote storage. Signed-off-by: krish <krish@lyzr.com>
Signed-off-by: krish <krish@lyzr.com>
Signed-off-by: krish <krish@lyzr.com>
roosterfish
left a comment
There was a problem hiding this comment.
Hi @krishvsoni, thanks for taking a look at this. Please see my comments.
| continue | ||
| } | ||
|
|
||
| // Exclude non-pristine disks with partitions. |
There was a problem hiding this comment.
This comments needs updating. Non-pristine disks are only excluded in case they are mounted.
There was a problem hiding this comment.
you are right, the comment is outdated now, Non-pristine disks are only dropped if they are mounted, the unmounted ones go to ReusableDisks.
| // Exclude non-pristine disks with partitions. | ||
| // Disks already used for local storage (zfs) contain a partition and are therefore excluded by this check. | ||
| // Unmounted ones are kept separately as they might contain an existing storage pool that can be reused. | ||
| if len(disk.Partitions) != 0 { |
There was a problem hiding this comment.
Why not using the IsPristineDisk func here?
There was a problem hiding this comment.
good point; I kept the plain partitions check on purpose so that disks with a filesystem but no partitions (e.g. a whole-disk btrfs/LVM) keep showing up in the Ceph disk list like today.
but that is inconsistent, and the docs already say storage disks should be free of partitions and filesystems.
I'll switch to IsPristineDisk so the split becomes:
if IsPristineDisk(disk) -> AvailableDisks
else if not mounted -> ReusableDisks (local storage only)
one side effect worth flagging: disks with only a filesystem will no longer be offered for distributed storage
(they can still be picked for local storage and reused or wiped there).
Is that fine with you @roosterfish ?
|
|
||
| // IsPristineDisk returns true if the disk has neither partitions nor a filesystem. | ||
| func IsPristineDisk(disk api.ResourcesStorageDisk) bool { | ||
| return disk.DeviceFSUUID == "" && len(disk.Partitions) == 0 |
There was a problem hiding this comment.
why not including isDiskMounted also in here?
There was a problem hiding this comment.
makes sense.
a mounted disk should never count as pristine, so I'll add it there. In practice a mounted disk already has a filesystem or partitions, but this makes it explicit and safer.
I'll still need isDiskMounted separately in the filter to decide whether a non-pristine disk is offered for reuse or skipped completely.
| for i, c := range cases { | ||
| s.T().Logf("%d: %s", i, c.desc) | ||
|
|
||
| s.Equal(c.pristine, IsPristineDisk(c.disk)) |
There was a problem hiding this comment.
As mentioned in the earlier comment, if isDiskMounted is included inside IsPristineDisk, then we can also check for the cases in case the disk or any of its partitions is mounted.
There was a problem hiding this comment.
got it, will add test cases for a mounted disk and for a disk with a mounted partition.
| Entity: "storage-pool", | ||
| Name: DefaultZFSPool, | ||
| Key: "source.recover", | ||
| Value: "true", |
There was a problem hiding this comment.
Doesn't this value need to be based on recoverSource? Otherwise it's always true.
There was a problem hiding this comment.
The key is only appended when go recoverSource is true:
if recoverSource {
joinConfig = append(joinConfig, recoverDisk)
}So it can never end up in the config as true unless recovery was explicitly requested. This follows the same pattern as the existing wipeDisk key just above it.
If the logic is confusing, I'm happy to build the key inside the if block instead to make the conditional behavior more obvious.
| continue | ||
| } | ||
|
|
||
| reuse, err := c.asker.AskBool(fmt.Sprintf("Disk %q on %q is not empty. Reuse its existing local storage pool?", selected[target], target), true) |
There was a problem hiding this comment.
Instead of empty, let's use the same terminology "non-pristine" instead of "not empty".
There was a problem hiding this comment.
will do: Disk %q on %q is non-pristine. Reuse its existing local storage pool?
|
|
||
| for _, disk := range system.Storage.Ceph { | ||
| if disk.Recover { | ||
| return fmt.Errorf("Cannot recover Ceph disks of system %q", system.Name) |
There was a problem hiding this comment.
Can this path be actually triggered?
There was a problem hiding this comment.
yes; DirectStorage is shared by both the local and Ceph entries, so the following configuration parses successfully:
storage:
ceph:
- path: /dev/sdc
recover: truewithout the check, recover would simply be silently ignored for Ceph disks.
that said, I noticed that encrypt is also silently ignored for local disks today, so silently ignoring unsupported fields would at least be consistent with the existing behavior.
would you prefer to keep the explicit validation error or remove it for consistency with encrypt? alternatively, I could introduce a separate struct for local storage so that recover is only available there.
| return fmt.Errorf("Missing the local storage disk path to recover on system %q", system.Name) | ||
| } | ||
|
|
||
| if system.Storage.Local.Wipe { |
There was a problem hiding this comment.
What about not asking to wipe if the user already selected to recover the disk?
There was a problem hiding this comment.
The interactive flow already excludes reused disks from the wipe selection. In preseed, both flags can be set explicitly, so I added an error to prevent conflicting actions and unintended data loss. Alternatively, I can make recover take precedence over wipe.
| } | ||
|
|
||
| if !recoverable { | ||
| return nil, errors.New("Cannot recover local storage disks: LXD is missing the storage_source_recover API extension") |
There was a problem hiding this comment.
This should never be surfaced to a user. The existence of the API extension should define whether or not a user can even recover non-pristine disks. This means if the API extension is absent, we should not even display non-pristine disks and continue as today with pristine disks only.
|
|
||
| The default file system type for local storage is ZFS. | ||
|
|
||
| When adding a cluster member, MicroCloud also offers unmounted disks that contain an existing local storage pool, for example after reinstalling a machine. |
There was a problem hiding this comment.
Let's use the "non-pristine" terminology here too.
A mounted disk is never pristine. Only pristine disks are available for storage, unmounted non-pristine disks are kept as reusable. Signed-off-by: krish <krish@lyzr.com>
The extension was added in LXD 6.7 and MicroCloud already requires at least LXD 6.10, so it is always present. Signed-off-by: krish <krish@lyzr.com>
Recovering an existing local storage pool is also useful when the whole cluster is set up again, so allow it both interactively and in the preseed when initializing MicroCloud. Signed-off-by: krish <krish@lyzr.com>
…rompt Signed-off-by: krish <krish@lyzr.com>
Signed-off-by: krish <krish@lyzr.com>
|
@roosterfish thanks for the detailed review. I pushed the changes, here is a summary per comment: Disk classification (
→ 1d15a2f API extension check
→ bd90b8d Reuse during init
→ 0b0a83f Terminology
source.recover in
Open questions, waiting for your preference
|
Fixes #1262
Summary
microcloud add(andmicrocloud join, whose disk questions the initiator answers) can now reuse an existing local storage pool on a disk. A rebuilt member can rejoin without wiping its data.Disk classification, following the issue and the maintainer's comment on it:
device_fs_uuidpartitions""[]""[]Local storage flow when adding systems (
askLocalPool):Notes:
source.recover(API extensionstorage_source_recover), notsource.reuseas written in the issue. It is a member-specific pool key, so it works in the join config. With it, LXD mounts/imports the existing pool instead of runningzpool create.!bootstrap) and when LXD hasstorage_source_recover.askLocalPoolended withif wipeable { toWipe = selectedDisks }, so every selected local disk was wiped regardless of the "Select which disks to wipe" answer (regression from 346d179). Without this fix, a disk selected for reuse would have been wiped.systems[].storage.local.recoverfield. Validation rejects it combined withwipe, withoutpath, on Ceph disks, and during init.Parsechecks for the LXD extension before using it.Commits:
Evidence
Unit tests, run on Linux (arm64, Go 1.27.1, dqlite built as in
make deps).go vet ./service/ ./cmd/microcloud/is clean.IsPristineDiskwith the issue's original rule (device_fs_uuid == ""only):len(partitions) == 0only):Not covered: the interactive flow has no automated test, and the change has not been exercised end to end on a real cluster. A system test in
test/suites(create a ZFS pool on a disk, remove the member, add it back with reuse, check the pool survives) can be added if wanted.Merge Danger
Door: two-way
Reuse never destroys data (default answer is yes; the wipe table excludes reused disks). The code reverts cleanly.
Blast Radius: local-storage
microcloud addcan show a new question. Scripted input (TEST_CONSOLE) only sees it when a selected disk is non-pristine. System test disks are wiped byreset_systems, so existing suites should be unaffected.DefaultPendingZFSStoragePoolandDefaultZFSStoragePoolJoinConfigtake a newrecoverSourceargument. All callers are updated.Checklist