-
Notifications
You must be signed in to change notification settings - Fork 16
Add Btrfs fallback for other Linux distros #956
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,6 @@ | |
| package lxdbackend | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "cmp" | ||
| "context" | ||
| "embed" | ||
|
|
@@ -58,6 +57,11 @@ const ( | |
| storagePool = "workshop" | ||
| storagePoolMinimalGiB = 5 | ||
|
|
||
| // ZFS is workshop's preferred storage driver; Btrfs is the fallback used | ||
| // on hosts where LXD reports ZFS as unavailable. | ||
| storageDriverZFS = "zfs" | ||
| storageDriverBtrfs = "btrfs" | ||
|
|
||
| networkName = "workshopbr0" | ||
| networkType = "bridge" | ||
|
|
||
|
|
@@ -73,31 +77,12 @@ const ( | |
|
|
||
| var ( | ||
| startCommandTimeout = 1 * time.Minute | ||
| storagePoolDriver = "zfs" | ||
| ) | ||
|
|
||
| //go:embed start_command.sh | ||
| var startCommand string | ||
|
|
||
| // isWSL checks if we're running on Windows Subsystem for Linux | ||
| func isWSL() bool { | ||
| var utsname unix.Utsname | ||
| if err := unix.Uname(&utsname); err != nil { | ||
| return false | ||
| } | ||
| data := utsname.Release[:] | ||
| if idx := bytes.IndexByte(data, 0); idx >= 0 { | ||
| data = data[:idx] | ||
| } | ||
| version := strings.ToLower(string(data)) | ||
| return strings.Contains(version, "microsoft") || strings.Contains(version, "wsl2") | ||
| } | ||
|
|
||
| func init() { | ||
| if isWSL() { | ||
| storagePoolDriver = "btrfs" | ||
| } | ||
|
|
||
| // Order matters: capabilities (version and storage) must be validated | ||
| // before ensureBackendReady attempts to create the storage pool and | ||
| // network. Registering it as a check also lets the daemon recover after | ||
|
|
@@ -179,19 +164,56 @@ func checkVersion(version string) error { | |
| return nil | ||
| } | ||
|
|
||
| func checkStorageDriver(drivers []api.ServerStorageDriverInfo) error { | ||
| hasDriver := func(driver api.ServerStorageDriverInfo) bool { | ||
| return driver.Name == storagePoolDriver | ||
| // driverSupported reports whether LXD lists the named storage driver as | ||
| // supported. LXD builds this list by attempting to load each driver's kernel | ||
| // module (e.g. modprobe zfs), so it reflects what the host can actually back. | ||
| func driverSupported(supported []api.ServerStorageDriverInfo, name string) bool { | ||
| return slices.ContainsFunc(supported, func(d api.ServerStorageDriverInfo) bool { | ||
| return d.Name == name | ||
| }) | ||
| } | ||
|
|
||
| // preferredDriver picks the driver for a new workshop pool: ZFS when LXD | ||
| // reports it as supported, otherwise Btrfs. | ||
| func preferredDriver(supported []api.ServerStorageDriverInfo) string { | ||
| if driverSupported(supported, storageDriverZFS) { | ||
| return storageDriverZFS | ||
| } | ||
| return storageDriverBtrfs | ||
| } | ||
|
|
||
| // poolUsesZFS reports whether the workshop storage pool is backed by ZFS. The | ||
| // driver is read from the pool itself, since it is fixed when the pool is | ||
| // created and can differ from what the host would pick today. | ||
| func poolUsesZFS(conn lxd.InstanceServer) (bool, error) { | ||
| pool, _, err := conn.GetStoragePool(storagePool) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| if slices.ContainsFunc(drivers, hasDriver) { | ||
| return pool.Driver == storageDriverZFS, nil | ||
| } | ||
|
|
||
| // checkStoragePool verifies the workshop storage pool is usable. When the pool | ||
| // exists but cannot be loaded, most likely because its storage driver's kernel | ||
| // module is gone (e.g. a ZFS pool after booting a kernel without the ZFS | ||
| // module), it returns an actionable error that puts the daemon into degraded | ||
| // mode. When the pool does not exist yet, it ensures a supported driver is | ||
| // available to create it with. | ||
| func checkStoragePool(conn lxd.InstanceServer, supported []api.ServerStorageDriverInfo) error { | ||
| _, _, err := conn.GetStoragePool(storagePool) | ||
| if err == nil { | ||
| return nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to check if the storage pool driver is supported here? |
||
| } | ||
| if !api.StatusErrorCheck(err, http.StatusNotFound) { | ||
| return fmt.Errorf(`cannot use the %q storage pool, its storage driver may be unavailable (for example a ZFS pool after booting a kernel without the ZFS module): %w | ||
| Boot into a kernel that provides the pool's storage driver, or reinstall Workshop to recreate the pool on an available driver`, storagePool, err) | ||
|
Comment on lines
+206
to
+207
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is the error returned from
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, from my tests, this is what you get: $ workshop list
error: system is not healthy: cannot use the "workshop" storage pool, its storage driver may be unavailable (for example a ZFS pool after booting a kernel without the ZFS module): Error loading "zfs" module: Failed running: modprobe -b zfs
: exit status 1 (modprobe: FATAL: Module zfs not found in directory /lib/modules/7.2.0-rc7)
Boot into a kernel that provides the pool's storage driver, or reinstall Workshop to recreate the pool on an available driver
So the error returned by
Perhaps the explanation should be less verbose? Since the error is already quite informative.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's interesting and a bit unexpected. I'd rather not rely on specific LXD error messages if possible, it's likely not something they have tests for. How about checking The error message is useful though, does it appear in the LXD logs? Maybe we can direct users there
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good point, I'll keep my explanation rather than trimming it in favor of LXD's message.
On using So On the logs: LXD only logs the "driver not available" line at A couple of options that I could think of:
I prefer (1) for this PR as the smaller change, but (2) is cleaner if we want to avoid depending on the LXD error at all. What do you think?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should definitely keep branching on 404. I would do the following:
|
||
| } | ||
|
|
||
| // The LXD error message when creating a pool is: | ||
| // Error: Error loading "zfs" module: Failed to run: modprobe -b zfs: | ||
| // exit status 1 (modprobe: FATAL: Module zfs not found ...) | ||
| // We keep the first part for consistency, the rest doesn't add much. | ||
| return fmt.Errorf(`suitable storage backend not found: error loading %q module`, storagePoolDriver) | ||
| // The pool doesn't exist yet; make sure it can be created. | ||
| if !driverSupported(supported, preferredDriver(supported)) { | ||
| return fmt.Errorf(`suitable storage backend not found: neither %q nor %q is available`, storageDriverZFS, storageDriverBtrfs) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // checkStorageSpace puts the daemon into degraded mode when the workshop | ||
|
|
@@ -253,7 +275,7 @@ func checkServerCapabilities() error { | |
| return err | ||
| } | ||
|
|
||
| return checkStorageDriver(info.Environment.StorageSupportedDrivers) | ||
| return checkStoragePool(conn, info.Environment.StorageSupportedDrivers) | ||
| } | ||
|
|
||
| // New constructs the LXD backend and attempts to prepare the required LXD | ||
|
|
@@ -284,15 +306,22 @@ func ensureBackendReady() error { | |
| } | ||
| defer conn.Disconnect() | ||
|
|
||
| // Create LXD storage pool if it doesn't exist. | ||
| pools, err := conn.GetStoragePools() | ||
| // Create LXD storage pool if it doesn't exist. GetStoragePoolNames avoids | ||
| // loading each pool's driver, so an existing but currently-unloadable pool | ||
| // (e.g. a ZFS pool whose module is gone) is reported by checkStoragePool | ||
| // rather than failing here. | ||
| names, err := conn.GetStoragePoolNames() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to list all the storage pools here? what stops us querying |
||
| if err != nil { | ||
| return err | ||
| } | ||
| if idx := slices.IndexFunc(pools, func(p api.StoragePool) bool { return p.Name == storagePool }); idx < 0 { | ||
| if !slices.Contains(names, storagePool) { | ||
| info, _, err := conn.GetServer() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| req := api.StoragePoolsPost{ | ||
| Name: storagePool, | ||
| Driver: storagePoolDriver, | ||
| Driver: preferredDriver(info.Environment.StorageSupportedDrivers), | ||
| } | ||
| op, err := conn.CreateStoragePool(req) | ||
| if err != nil { | ||
|
|
@@ -331,8 +360,6 @@ func ensureBackendReady() error { | |
|
|
||
| logger.Noticef("On ensureBackendReady: set storage pool to the minimal size: %dGiB", storagePoolMinimalGiB) | ||
| } | ||
| } else if pools[idx].Driver != storagePoolDriver { | ||
| return fmt.Errorf("storage pool %q already exists with a different driver: %q (expected %q)", storagePool, pools[idx].Driver, storagePoolDriver) | ||
| } | ||
|
|
||
| network, etag, err := conn.GetNetwork(networkName) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -346,7 +346,11 @@ func (s *Backend) TakeSnapshot(ctx context.Context, name string, snapshot worksh | |
| if inst.Devices == nil { | ||
| inst.Devices = map[string]map[string]string{} | ||
| } | ||
| if err := mergeDevices(inst.Devices, snapshot.Sdks, name); err != nil { | ||
| usesZFS, err := poolUsesZFS(conn) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this adds an additional API call to these operations, do we have any info on the impact it has on latency? we could alternatively cache the storage pool driver at launch |
||
| if err != nil { | ||
| return err | ||
| } | ||
| if err := mergeDevices(inst.Devices, snapshot.Sdks, name, usesZFS); err != nil { | ||
| return err | ||
| } | ||
|
|
||
|
|
@@ -604,12 +608,12 @@ func mergeConfig(source, target, config map[string]string) { | |
| maps.Copy(source, config) | ||
| } | ||
|
|
||
| func mergeDevices(source map[string]map[string]string, sdks []sdk.ContentID, w string) error { | ||
| func mergeDevices(source map[string]map[string]string, sdks []sdk.ContentID, w string, usesZFS bool) error { | ||
| maps.DeleteFunc(source, func(k string, v map[string]string) bool { | ||
| return k != "root" | ||
| }) | ||
|
|
||
| if storagePoolDriver == "zfs" { | ||
| if usesZFS { | ||
| root := maps.Clone(source["root"]) | ||
| if source == nil || root == nil { | ||
| return fmt.Errorf("internal error: %q workshop has no rootfs", w) | ||
|
|
@@ -740,7 +744,11 @@ func (s *Backend) copyInstance(src, dst lxd.InstanceServer, srcName, dstName str | |
|
|
||
| req := *srcInst | ||
|
|
||
| if storagePoolDriver == "zfs" { | ||
| usesZFS, err := poolUsesZFS(dst) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if usesZFS { | ||
| req.Devices = maps.Clone(req.Devices) | ||
| root := maps.Clone(req.Devices["root"]) | ||
| if req.Devices == nil || root == nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see what we gain from these constants