From 2c027644b1994e9c9c51772d58d24b88a9cd60fa Mon Sep 17 00:00:00 2001 From: Iwan BK Date: Fri, 5 Jul 2024 10:20:01 +0700 Subject: [PATCH 1/2] feat(bcachefs): initial implementation of bcachefs. The goal is not to have working implementation. But to give clearer picture about what we have to do to support bcachefs, code wise. --- pkg/storage/filesystem/bcachefs.go | 323 +++++++++++++++++++++++ pkg/storage/filesystem/bcachefs_utils.go | 23 ++ 2 files changed, 346 insertions(+) create mode 100644 pkg/storage/filesystem/bcachefs.go create mode 100644 pkg/storage/filesystem/bcachefs_utils.go diff --git a/pkg/storage/filesystem/bcachefs.go b/pkg/storage/filesystem/bcachefs.go new file mode 100644 index 000000000..0ac320e54 --- /dev/null +++ b/pkg/storage/filesystem/bcachefs.go @@ -0,0 +1,323 @@ +package filesystem + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "syscall" + + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/threefoldtech/zos/pkg/gridtypes/zos" +) + +// NewBcachefsPool creates a btrfs pool associated with device. +// if device does not have a filesystem one is created +func NewBcachefsPool(device DeviceInfo) (Pool, error) { + return newBcachefsPool(device, executerFunc(run)) +} + +func newBcachefsPool(device DeviceInfo, exe executer) (Pool, error) { + pool := &bcachefsPool{ + device: device, + utils: newBcachefsCmd(exe), + name: device.Label, + } + + return pool, pool.prepare() +} + +var ( + errNotImplemented = errors.New("not implemented") +) + +type bcachefsPool struct { + device DeviceInfo + utils bcachefsUtils + name string +} + +func (p *bcachefsPool) prepare() error { + // check if already have filesystem + if p.device.Used() { + return nil + } + ctx := context.TODO() + + // otherwise format + if err := p.format(ctx); err != nil { + return err + } + // make sure kernel knows about this + return Partprobe(ctx) +} + +func (p *bcachefsPool) format(ctx context.Context) error { + name := uuid.New().String() + p.name = name + + args := []string{ + "-L", name, + p.device.Path, + } + + if _, err := p.utils.run(ctx, "mkfs.bcachefs", args...); err != nil { + return errors.Wrapf(err, "failed to format device '%s'", p.device.Path) + } + + return nil +} + +// Volume ID +func (b *bcachefsPool) ID() int { + return 0 +} + +// Path of the volume +func (b *bcachefsPool) Path() string { + return filepath.Join("/mnt", b.name) +} + +// Usage returns the pool usage +func (b *bcachefsPool) Usage() (Usage, error) { + return Usage{}, errNotImplemented +} + +// Limit on a pool is not supported yet +func (b *bcachefsPool) Limit(size uint64) error { + return errNotImplemented +} + +// Name of the volume +func (b *bcachefsPool) Name() string { + return b.name +} + +// FsType of the volume +func (b *bcachefsPool) FsType() string { + return "bcachefs" +} + +// Mounted returns whether the pool is mounted or not. If it is mounted, +// the mountpoint is returned +// It doesn't check the default mount location of the pool +// but instead check if any of the pool devices is mounted +// under any location +func (p *bcachefsPool) Mounted() (string, error) { + ctx := context.TODO() + mnt, err := p.device.Mountpoint(ctx) + if err != nil { + return "", err + } + + if len(mnt) != 0 { + return mnt, nil + } + + return "", ErrDeviceNotMounted +} + +// Mount the pool, the mountpoint is returned +func (p *bcachefsPool) Mount() (string, error) { + mnt, err := p.Mounted() + if err == nil { + return mnt, nil + } else if !errors.Is(err, ErrDeviceNotMounted) { + return "", errors.Wrap(err, "failed to check device mount status") + } + + // device is not mounted + mnt = p.Path() + if err := os.MkdirAll(mnt, 0755); err != nil { + return "", err + } + + if err := syscall.Mount(p.device.Path, mnt, "bcachefs", 0, ""); err != nil { + return "", err + } + + // TODO: check + //if err := p.utils.QGroupEnable(ctx, mnt); err != nil { + // return "", fmt.Errorf("failed to enable qgroup: %w", err) + //} + + return mnt, p.maintenance() +} + +func (p *bcachefsPool) maintenance() error { + return errNotImplemented +} + +// UnMount the pool +func (p *bcachefsPool) UnMount() error { + mnt, err := p.Mounted() + if err != nil { + if errors.Is(err, ErrDeviceNotMounted) { + return nil + } + return err + } + + return syscall.Unmount(mnt, syscall.MNT_DETACH) +} + +// Volumes are all subvolumes of this volume +// TODO: bcachefs doesn't have the feature +func (p *bcachefsPool) Volumes() ([]Volume, error) { + return nil, errNotImplemented +} + +// AddVolume adds a new subvolume with the given name +func (p *bcachefsPool) AddVolume(name string) (Volume, error) { + mnt, err := p.Mounted() + if err != nil { + return nil, err + } + + root := filepath.Join(mnt, name) + return p.addVolume(root) +} + +func (p *bcachefsPool) addVolume(root string) (Volume, error) { + ctx := context.Background() + if err := p.utils.SubvolumeAdd(ctx, root); err != nil { + return nil, err + } + + //volume, err := p.utils.SubvolumeInfo(ctx, root) + //if err != nil { + // return nil, err + //} + return &bcachefsVolume{ + id: 0, + path: root, + }, nil +} + +// RemoveVolume removes a subvolume with the given name +func (b *bcachefsPool) RemoveVolume(name string) error { + mnt, err := b.Mounted() + if err != nil { + return err + } + + root := filepath.Join(mnt, name) + return b.removeVolume(root) +} + +func (p *bcachefsPool) removeVolume(root string) error { + ctx := context.Background() + + //info, err := p.utils.SubvolumeInfo(ctx, root) + //if err != nil { + // return err + //} + + if err := p.utils.SubvolumeRemove(ctx, root); err != nil { + return err + } + + /*qgroupID := fmt.Sprintf("0/%d", info.ID) + if err := p.utils.QGroupDestroy(ctx, qgroupID, p.Path()); err != nil { + // we log here and not return an error because + // - qgroup deletion can fail because it is still used by the system + // even if the volume is gone + // - failure to delete a qgroup is not a fatal error + log.Warn().Err(err).Str("group-id", qgroupID).Msg("failed to delete qgroup") + return nil + }*/ + + return nil +} + +// Shutdown spins down the device where the pool is mounted +func (b *bcachefsPool) Shutdown() error { + cmd := exec.Command("hdparm", "-y", b.device.Path) + if err := cmd.Run(); err != nil { + return errors.Wrapf(err, "failed to shutdown device '%s'", b.device.Path) + } + + return nil +} + +// Device return device associated with pool +func (b *bcachefsPool) Device() DeviceInfo { + return b.device +} + +// SetType sets a device type on the pool. this will make +// sure that the detected device type is reported +// correctly by calling the Type() method. +// TODO : merge the code with btrfs +func (b *bcachefsPool) SetType(typ zos.DeviceType) error { + path, err := b.Mounted() + if err != nil { + return err + } + diskTypePath := filepath.Join(path, ".seektime") + if err := os.WriteFile(diskTypePath, []byte(typ), 0644); err != nil { + return errors.Wrapf(err, "failed to store device type for '%s' in '%s'", b.Name(), diskTypePath) + } + + return nil +} + +// Type returns the device type set by a previous call +// to SetType. +// TODO : merge the code with btrfs +func (b *bcachefsPool) Type() (zos.DeviceType, bool, error) { + path, err := b.Mounted() + if err != nil { + return "", false, err + } + diskTypePath := filepath.Join(path, ".seektime") + diskType, err := os.ReadFile(diskTypePath) + if os.IsNotExist(err) { + return "", false, nil + } + + if err != nil { + return "", false, err + } + + if len(diskType) == 0 { + return "", false, nil + } + + return zos.DeviceType(diskType), true, nil +} + +type bcachefsVolume struct { + id int + path string +} + +func (v *bcachefsVolume) ID() int { + return v.id +} + +func (v *bcachefsVolume) Path() string { + return v.path +} + +// Name of the filesystem +func (v *bcachefsVolume) Name() string { + return filepath.Base(v.Path()) +} + +// FsType of the filesystem +func (v *bcachefsVolume) FsType() string { + return "bcachefs" +} + +// Usage return the volume usage +func (v *bcachefsVolume) Usage() (usage Usage, err error) { + err = errNotImplemented + return +} + +// Limit size of volume, setting size to 0 means unlimited +func (v *bcachefsVolume) Limit(size uint64) error { + return errNotImplemented +} diff --git a/pkg/storage/filesystem/bcachefs_utils.go b/pkg/storage/filesystem/bcachefs_utils.go new file mode 100644 index 000000000..3b56f6865 --- /dev/null +++ b/pkg/storage/filesystem/bcachefs_utils.go @@ -0,0 +1,23 @@ +package filesystem + +import "context" + +type bcachefsUtils struct { + executer +} + +func newBcachefsCmd(exec executer) bcachefsUtils { + return bcachefsUtils{exec} +} + +// SubvolumeAdd adds a new subvolume at path +func (u *bcachefsUtils) SubvolumeAdd(ctx context.Context, root string) error { + _, err := u.run(ctx, "bcachefs", "subvolume", "create", root) + return err +} + +// SubvolumeRemove removes a subvolume +func (u *bcachefsUtils) SubvolumeRemove(ctx context.Context, root string) error { + _, err := u.run(ctx, "bcachefs", "subvolume", "delete", root) + return err +} From a4938cfb1d9233360435f0433441fc47face901b Mon Sep 17 00:00:00 2001 From: Iwan BK Date: Fri, 9 Aug 2024 18:12:53 +0700 Subject: [PATCH 2/2] checkpoint --- pkg/storage/disk.go | 33 +++- pkg/storage/filesystem/bcachefs.go | 95 ++++----- pkg/storage/filesystem/bcachefs_utils.go | 237 ++++++++++++++++++++++- pkg/storage/filesystem/device.go | 3 +- pkg/storage/storage.go | 3 +- 5 files changed, 318 insertions(+), 53 deletions(-) diff --git a/pkg/storage/disk.go b/pkg/storage/disk.go index f9083717c..0449a5aae 100644 --- a/pkg/storage/disk.go +++ b/pkg/storage/disk.go @@ -14,6 +14,7 @@ import ( log "github.com/rs/zerolog/log" "github.com/threefoldtech/zos/pkg" "github.com/threefoldtech/zos/pkg/gridtypes" + "golang.org/x/sync/errgroup" ) const ( @@ -145,10 +146,38 @@ func (s *Module) DiskWrite(name string, image string) error { return fmt.Errorf("image size is bigger than disk") } - _, err = io.Copy(file, source) - if err != nil { + var ( + g = new(errgroup.Group) + concurrentNUm int = 5 + imgSize int64 = imgStat.Size() + chunkSize = imgSize / int64(concurrentNUm) + ) + + log.Info().Int("concurrentNum", concurrentNUm).Msg("writing image concurrently") + + for i := 0; i < concurrentNUm; i++ { + index := i + g.Go(func() error { + start := chunkSize * int64(index) + len := chunkSize + if index == concurrentNUm-1 { + len = imgSize - start + } + wr := io.NewOffsetWriter(file, start) + rd := io.NewSectionReader(source, start, len) + _, err = io.Copy(wr, rd) + return err + }) + } + if err := g.Wait(); err != nil { return errors.Wrap(err, "failed to write disk image") } + log.Info().Msg("writing image finished") + + /*_, err = io.Copy(file, source) + if err != nil { + return errors.Wrap(err, "failed to write disk image") + }*/ return nil } diff --git a/pkg/storage/filesystem/bcachefs.go b/pkg/storage/filesystem/bcachefs.go index 0ac320e54..de9854972 100644 --- a/pkg/storage/filesystem/bcachefs.go +++ b/pkg/storage/filesystem/bcachefs.go @@ -2,6 +2,7 @@ package filesystem import ( "context" + "fmt" "os" "os/exec" "path/filepath" @@ -9,6 +10,7 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/threefoldtech/zos/pkg/gridtypes/zos" ) @@ -39,6 +41,7 @@ type bcachefsPool struct { } func (p *bcachefsPool) prepare() error { + //p.name = uuid.New().String() //p.device.Label TODO // check if already have filesystem if p.device.Used() { return nil @@ -61,6 +64,7 @@ func (p *bcachefsPool) format(ctx context.Context) error { "-L", name, p.device.Path, } + log.Info().Str("device", p.device.Path).Msg("formatting device") if _, err := p.utils.run(ctx, "mkfs.bcachefs", args...); err != nil { return errors.Wrapf(err, "failed to format device '%s'", p.device.Path) @@ -80,13 +84,36 @@ func (b *bcachefsPool) Path() string { } // Usage returns the pool usage -func (b *bcachefsPool) Usage() (Usage, error) { - return Usage{}, errNotImplemented +func (b *bcachefsPool) Usage() (usage Usage, err error) { + mnt, err := b.Mounted() + if err != nil { + return usage, err + } + + volumes, err := b.Volumes() + + if err != nil { + return usage, errors.Wrapf(err, "failed to list pool '%s' volumes", mnt) + } + + usage.Size = b.device.Size + + for _, volume := range volumes { + vol, err := volume.Usage() + if err != nil { + return Usage{}, errors.Wrapf(err, "failed to calculate volume '%s' usage", volume.Path()) + } + + usage.Used += vol.Used + usage.Excl += vol.Excl + } + + return } // Limit on a pool is not supported yet func (b *bcachefsPool) Limit(size uint64) error { - return errNotImplemented + return errNotImplemented // btrfs also doesn't support this } // Name of the volume @@ -146,7 +173,8 @@ func (p *bcachefsPool) Mount() (string, error) { } func (p *bcachefsPool) maintenance() error { - return errNotImplemented + // TODO + return nil } // UnMount the pool @@ -165,7 +193,22 @@ func (p *bcachefsPool) UnMount() error { // Volumes are all subvolumes of this volume // TODO: bcachefs doesn't have the feature func (p *bcachefsPool) Volumes() ([]Volume, error) { - return nil, errNotImplemented + mnt, err := p.Mounted() + if err != nil { + return nil, err + } + + var volumes []Volume + + subs, err := p.utils.SubvolumeList(context.Background(), mnt) + if err != nil { + return nil, fmt.Errorf("subvolumelist failed: %v", err) + } + + for _, sub := range subs { + volumes = append(volumes, sub.ToStorageVolume(mnt)) + } + return volumes, nil } // AddVolume adds a new subvolume with the given name @@ -181,7 +224,8 @@ func (p *bcachefsPool) AddVolume(name string) (Volume, error) { func (p *bcachefsPool) addVolume(root string) (Volume, error) { ctx := context.Background() - if err := p.utils.SubvolumeAdd(ctx, root); err != nil { + vol, err := p.utils.SubvolumeAdd(ctx, root) + if err != nil { return nil, err } @@ -189,10 +233,7 @@ func (p *bcachefsPool) addVolume(root string) (Volume, error) { //if err != nil { // return nil, err //} - return &bcachefsVolume{ - id: 0, - path: root, - }, nil + return &vol, nil } // RemoveVolume removes a subvolume with the given name @@ -287,37 +328,3 @@ func (b *bcachefsPool) Type() (zos.DeviceType, bool, error) { return zos.DeviceType(diskType), true, nil } - -type bcachefsVolume struct { - id int - path string -} - -func (v *bcachefsVolume) ID() int { - return v.id -} - -func (v *bcachefsVolume) Path() string { - return v.path -} - -// Name of the filesystem -func (v *bcachefsVolume) Name() string { - return filepath.Base(v.Path()) -} - -// FsType of the filesystem -func (v *bcachefsVolume) FsType() string { - return "bcachefs" -} - -// Usage return the volume usage -func (v *bcachefsVolume) Usage() (usage Usage, err error) { - err = errNotImplemented - return -} - -// Limit size of volume, setting size to 0 means unlimited -func (v *bcachefsVolume) Limit(size uint64) error { - return errNotImplemented -} diff --git a/pkg/storage/filesystem/bcachefs_utils.go b/pkg/storage/filesystem/bcachefs_utils.go index 3b56f6865..aaa69628e 100644 --- a/pkg/storage/filesystem/bcachefs_utils.go +++ b/pkg/storage/filesystem/bcachefs_utils.go @@ -1,23 +1,250 @@ package filesystem -import "context" +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" +) type bcachefsUtils struct { executer + volManager bcachefsVolumeManager } func newBcachefsCmd(exec executer) bcachefsUtils { - return bcachefsUtils{exec} + return bcachefsUtils{ + executer: exec, + volManager: newBcachefsVolumeManager(), + } } // SubvolumeAdd adds a new subvolume at path -func (u *bcachefsUtils) SubvolumeAdd(ctx context.Context, root string) error { +func (u *bcachefsUtils) SubvolumeAdd(ctx context.Context, root string) (bcachefsVolume, error) { _, err := u.run(ctx, "bcachefs", "subvolume", "create", root) - return err + if err != nil { + return bcachefsVolume{}, err + } + return u.volManager.Add(root) } // SubvolumeRemove removes a subvolume func (u *bcachefsUtils) SubvolumeRemove(ctx context.Context, root string) error { _, err := u.run(ctx, "bcachefs", "subvolume", "delete", root) - return err + if err != nil { + return err + } + return u.volManager.Delete(root) +} + +func (u *bcachefsUtils) SubvolumeList(ctx context.Context, root string) ([]bcachefsVolume, error) { + return u.volManager.list(root) +} + +func (u *bcachefsUtils) SubvolumeInfo(ctx context.Context, root string) (bcachefsVolume, error) { + return u.volManager.Get(root) +} + +// bcachefs volume menager is a hack for the minimal support of sublvolume in bcachefs. +// it does several things: +// - keep track of the subvolumes created by the driver +// - listing subvolumes +// +// current implementation is considered a hack and should be replaced with a proper implementation +// before going to production +type bcachefsVolumeManager struct { + //root string + //vols map[string]bcachefsVolume +} + +type bcachefsSubvolumes map[string]bcachefsVolume + +func (vols bcachefsSubvolumes) add(vol bcachefsVolume) { + vols[vol.Name()] = vol +} + +func (vols bcachefsSubvolumes) get(root string) (bcachefsVolume, bool) { + v, ok := vols[root] + return v, ok +} + +func (vols bcachefsSubvolumes) delete(root string) { + delete(vols, root) +} + +func newBcachefsVolumeManager() bcachefsVolumeManager { + return bcachefsVolumeManager{} +} + +func (m bcachefsVolumeManager) metaFile(root string) string { + const ( + metaFileName = ".volumes" + ) + return filepath.Join(filepath.Dir(root), metaFileName) +} +func (m bcachefsVolumeManager) Add(root string) (bcachefsVolume, error) { + log.Info().Str("root", root).Msg("Add volume") + vols, err := m.load(root) + if err != nil { + return bcachefsVolume{}, fmt.Errorf("Add failed: %v", err) + } + vol := newBcachefsVol(0, root, 0, m) + vols.add(vol) + return vol, m.save(root, vols) +} + +func (m bcachefsVolumeManager) Delete(root string) error { + log.Info().Str("root", root).Msg("Delete volume") + vols, err := m.load(root) + if err != nil { + return fmt.Errorf("Delete failed: %v", err) + } + vols.delete(root) + return m.save(root, vols) +} + +func (m bcachefsVolumeManager) Get(root string) (vol bcachefsVolume, err error) { + log.Info().Str("root", root).Msg("Get volume") + vols, err := m.load(root) + if err != nil { + err = fmt.Errorf("failed to get volume %v: %v", root, err) + return + } + vol, ok := vols.get(root) + if !ok { + err = fmt.Errorf("volume %s not found", root) + return + } + vol.mgr = m + return +} + +func (m bcachefsVolumeManager) Set(vol bcachefsVolume) error { + root := vol.path + log.Info().Str("root", root).Msg("Set volume") + vols, err := m.load(root) + if err != nil { + return fmt.Errorf("Set failed: %v", err) + } + vols[vol.Name()] = vol + return m.save(root, vols) +} + +func (m bcachefsVolumeManager) list(root string) ([]bcachefsVolume, error) { + log.Info().Str("root", root).Msg("List volume") + vols, err := m.load(root + "/x") // TODO fix this ugly append hack + if err != nil { + return nil, fmt.Errorf("list failed: %v", err) + } + res := make([]bcachefsVolume, 0, len(vols)) + for name, v := range vols { + v.path = name + res = append(res, v) + } + return res, nil +} + +func (m bcachefsVolumeManager) load(root string) (bcachefsSubvolumes, error) { + f, err := os.OpenFile(m.metaFile(root), os.O_RDONLY, 0644) + if err != nil { + log.Error().Err(err).Str("root", root).Msg("failed to open .volumes file") + if !m.isMetaExists(root) { + log.Info().Msg("meta not exists") + return bcachefsSubvolumes{}, m.initialize(root) + } + log.Info().Msg("meta exists") + return nil, err + } + defer f.Close() + vols := bcachefsSubvolumes{} + err = json.NewDecoder(f).Decode(&vols) + return vols, err +} + +func (m *bcachefsVolumeManager) isMetaExists(root string) bool { + // Use os.Stat to get file information + _, err := os.Stat(m.metaFile(root)) + return !os.IsNotExist(err) +} + +func (m *bcachefsVolumeManager) initialize(root string) error { + return m.save(root, bcachefsSubvolumes{}) +} + +func (m bcachefsVolumeManager) save(root string, vols bcachefsSubvolumes) error { + f, err := os.OpenFile(m.metaFile(root), os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + return json.NewEncoder(f).Encode(vols) +} + +type bcachefsVolume struct { + id int + path string + Size uint64 + mgr bcachefsVolumeManager +} + +func newBcachefsVol(id int, path string, size uint64, mgr bcachefsVolumeManager) bcachefsVolume { + return bcachefsVolume{ + id: id, + path: path, + Size: size, + mgr: mgr, + } +} + +func (v *bcachefsVolume) ToStorageVolume(mnt string) Volume { + return &bcachefsVolume{ + id: v.id, + path: filepath.Join(mnt, v.Name()), + Size: v.Size, + mgr: v.mgr, + } +} + +func (v *bcachefsVolume) ID() int { + return v.id +} + +func (v *bcachefsVolume) Path() string { + return v.path +} + +// Name of the filesystem +func (v *bcachefsVolume) Name() string { + return filepath.Base(v.Path()) +} + +// FsType of the filesystem +func (v *bcachefsVolume) FsType() string { + return "bcachefs" +} + +// Usage return the volume usage +func (v *bcachefsVolume) Usage() (usage Usage, err error) { + used := v.Size + if used == 0 { + // in case no limit is set on the subvolume, we assume + // it's size is the size of the files on that volumes + // or a special case when the volume is a zdb volume + used, err = volumeUsage(v.Path()) + if err != nil { + return usage, errors.Wrap(err, "failed to get subvolume usage") + } + } + + return Usage{Used: used, Size: v.Size, Excl: 0}, nil +} + +// Limit size of volume, setting size to 0 means unlimited +func (v *bcachefsVolume) Limit(size uint64) error { + v.Size = size + return v.mgr.Set(*v) } diff --git a/pkg/storage/filesystem/device.go b/pkg/storage/filesystem/device.go index 7eabff1f0..ba8308617 100644 --- a/pkg/storage/filesystem/device.go +++ b/pkg/storage/filesystem/device.go @@ -218,7 +218,8 @@ func (l *lsblkDeviceManager) Mountpoint(ctx context.Context, device string) (str } for _, m := range mounts.Filesystems { - if subvolFindmntOption.MatchString(m.Options) { + //if subvolFindmntOption.MatchString(m.Options) { + if m.Source == device { return m.Target, nil } } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index a4b12c45f..f0ce04336 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -226,7 +226,8 @@ func (s *Module) initialize(ctx context.Context) error { for _, device := range devices { log.Debug().Msgf("device: %+v", device) - pool, err := filesystem.NewBtrfsPool(device) + //pool, err := filesystem.NewBtrfsPool(device) + pool, err := filesystem.NewBcachefsPool(device) if err != nil { log.Error().Err(err).Str("device", device.Path).Msg("failed to create pool on device") s.brokenDevices = append(s.brokenDevices, pkg.BrokenDevice{Path: device.Path, Err: err})