diff --git a/cmd/list.go b/cmd/list.go index fc2f122e..7692dd3b 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,11 +1,12 @@ package cmd import ( + "fmt" "os" "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/define" "github.com/containers/podman-bootc/pkg/user" - "github.com/containers/podman-bootc/pkg/utils" "github.com/containers/podman-bootc/pkg/vm" "github.com/containers/common/pkg/report" @@ -60,46 +61,49 @@ func doList(_ *cobra.Command, _ []string) error { } func CollectVmList(user user.User, libvirtUri string) (vmList []vm.BootcVMConfig, err error) { - files, err := os.ReadDir(user.CacheDir()) + ids, err := user.Storage().List() if err != nil { return nil, err } - for _, f := range files { - if f.IsDir() { - cfg, err := getVMInfo(user, libvirtUri, f.Name()) - if err != nil { - logrus.Warningf("skipping vm %s reason: %v", f.Name(), err) - continue - } - - vmList = append(vmList, *cfg) + for _, id := range ids { + cfg, err := getVMInfo(user, libvirtUri, id) + if err != nil { + logrus.Warningf("skipping vm %s reason: %v", id, err) + continue } + + vmList = append(vmList, *cfg) } return vmList, nil } -func getVMInfo(user user.User, libvirtUri string, imageId string) (*vm.BootcVMConfig, error) { +func getVMInfo(user user.User, libvirtUri string, imageId define.FullImageId) (*vm.BootcVMConfig, error) { + guard, unlock, err := user.Storage().Get(imageId) + if err != nil { + return nil, fmt.Errorf("unable to lock the VM cache: %w", err) + } + defer func() { + if err := unlock(); err != nil { + logrus.Warningf("unable to unlock VM %s: %v", imageId, err) + } + }() + bootcVM, err := vm.NewVM(vm.NewVMParameters{ - ImageID: imageId, + ImageID: string(imageId), User: user, LibvirtUri: libvirtUri, - Locking: utils.Shared, }) if err != nil { return nil, err } - // Let's be explicit instead of relying on the defer exec order defer func() { bootcVM.CloseConnection() - if err := bootcVM.Unlock(); err != nil { - logrus.Warningf("unable to unlock VM %s: %v", imageId, err) - } }() - cfg, err := bootcVM.GetConfig() + cfg, err := bootcVM.GetConfig(guard) if err != nil { return nil, err } diff --git a/cmd/rm.go b/cmd/rm.go index b6fc09d2..c43051bd 100644 --- a/cmd/rm.go +++ b/cmd/rm.go @@ -2,11 +2,10 @@ package cmd import ( "fmt" - "os" "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/define" "github.com/containers/podman-bootc/pkg/user" - "github.com/containers/podman-bootc/pkg/utils" "github.com/containers/podman-bootc/pkg/vm" "github.com/sirupsen/logrus" @@ -44,35 +43,49 @@ func oneOrAll() cobra.PositionalArgs { } func doRemove(_ *cobra.Command, args []string) error { + usr, err := user.NewUser() + if err != nil { + return err + } + if removeAll { - return pruneAll() + return pruneAll(usr) } - return prune(args[0]) + id := args[0] + fullImageId, err := usr.Storage().SearchByPrefix(id) + if err != nil { + return fmt.Errorf("searching for ID %s: %w", id, err) + } + if fullImageId == nil { + return fmt.Errorf("local installation '%s' does not exists", id) + } + + return prune(usr, *fullImageId) } -func prune(id string) error { - user, err := user.NewUser() +func prune(usr user.User, id define.FullImageId) error { + _, unlock, err := usr.Storage().GetExclusiveOrAdd(id) if err != nil { - return err + return fmt.Errorf("unable to lock the VM cache: %w", err) } + defer func() { + if err := unlock(); err != nil { + logrus.Errorf("unable to unlock VM %s: %v", id, err) + } + }() bootcVM, err := vm.NewVM(vm.NewVMParameters{ - ImageID: id, + ImageID: string(id), LibvirtUri: config.LibvirtUri, - User: user, - Locking: utils.Exclusive, + User: usr, }) if err != nil { return fmt.Errorf("unable to get VM %s: %v", id, err) } - // Let's be explicit instead of relying on the defer exec order defer func() { bootcVM.CloseConnection() - if err := bootcVM.Unlock(); err != nil { - logrus.Warningf("unable to unlock VM %s: %v", id, err) - } }() if force { @@ -90,24 +103,16 @@ func prune(id string) error { return nil } -func pruneAll() error { - user, err := user.NewUser() +func pruneAll(usr user.User) error { + ids, err := usr.Storage().List() if err != nil { return err } - files, err := os.ReadDir(user.CacheDir()) - if err != nil { - return err - } - - for _, f := range files { - if f.IsDir() { - vmID := f.Name() - err := prune(vmID) - if err != nil { - logrus.Errorf("unable to remove %s: %v", vmID, err) - } + for _, id := range ids { + err := prune(usr, id) + if err != nil { + logrus.Errorf("unable to remove %s: %v", id, err) } } diff --git a/cmd/run.go b/cmd/run.go index 021a2ac3..c521aac5 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -10,6 +10,7 @@ import ( "github.com/containers/podman-bootc/pkg/bootc" "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/define" "github.com/containers/podman-bootc/pkg/user" "github.com/containers/podman-bootc/pkg/utils" "github.com/containers/podman-bootc/pkg/vm" @@ -116,11 +117,20 @@ func doRun(flags *cobra.Command, args []string) error { return fmt.Errorf("unable to get free port for SSH: %w", err) } + guard, unlock, err := user.Storage().Get(define.FullImageId(bootcDisk.GetImageId())) + if err != nil { + return fmt.Errorf("unable to lock the VM cache: %w", err) + } + defer func() { + if err := unlock(); err != nil { + logrus.Warningf("unable to unlock VM %s: %v", bootcDisk.GetImageId(), err) + } + }() + bootcVM, err := vm.NewVM(vm.NewVMParameters{ ImageID: bootcDisk.GetImageId(), User: user, LibvirtUri: config.LibvirtUri, - Locking: utils.Shared, }) if err != nil { @@ -130,9 +140,6 @@ func doRun(flags *cobra.Command, args []string) error { // Let's be explicit instead of relying on the defer exec order defer func() { bootcVM.CloseConnection() - if err := bootcVM.Unlock(); err != nil { - logrus.Warningf("unable to unlock VM %s: %v", bootcDisk.GetImageId(), err) - } }() cmd := args[1:] @@ -153,6 +160,7 @@ func doRun(flags *cobra.Command, args []string) error { } // write down the config file + // FIXME: we are writing the config using a shared guard if err = bootcVM.WriteConfig(*bootcDisk); err != nil { return err } @@ -191,7 +199,7 @@ func doRun(flags *cobra.Command, args []string) error { } // ssh into the VM - ExitCode, err = utils.WithExitCode(bootcVM.RunSSH(cmd)) + ExitCode, err = utils.WithExitCode(bootcVM.RunSSH(guard, cmd)) if err != nil { return fmt.Errorf("ssh: %w", err) } diff --git a/cmd/ssh.go b/cmd/ssh.go index b6a9d9c0..2a81d070 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/containers/podman-bootc/pkg/config" "github.com/containers/podman-bootc/pkg/user" "github.com/containers/podman-bootc/pkg/utils" @@ -25,30 +27,41 @@ func init() { } func doSsh(_ *cobra.Command, args []string) error { - user, err := user.NewUser() + usr, err := user.NewUser() if err != nil { return err } id := args[0] + fullImageId, err := usr.Storage().SearchByPrefix(id) + if err != nil { + return fmt.Errorf("searching for ID %s: %w", id, err) + } + if fullImageId == nil { + return fmt.Errorf("local installation '%s' does not exists", id) + } + + guard, unlock, err := usr.Storage().Get(*fullImageId) + if err != nil { + return fmt.Errorf("unable to lock the VM cache: %w", err) + } + defer func() { + if err := unlock(); err != nil { + logrus.Warningf("unable to unlock VM %s: %v", id, err) + } + }() vm, err := vm.NewVM(vm.NewVMParameters{ - ImageID: id, - User: user, + ImageID: string(*fullImageId), + User: usr, LibvirtUri: config.LibvirtUri, - Locking: utils.Shared, }) if err != nil { return err } - - // Let's be explicit instead of relying on the defer exec order defer func() { vm.CloseConnection() - if err := vm.Unlock(); err != nil { - logrus.Warningf("unable to unlock VM %s: %v", id, err) - } }() err = vm.SetUser(sshUser) @@ -61,6 +74,6 @@ func doSsh(_ *cobra.Command, args []string) error { cmd = args[1:] } - ExitCode, err = utils.WithExitCode(vm.RunSSH(cmd)) + ExitCode, err = utils.WithExitCode(vm.RunSSH(guard, cmd)) return err } diff --git a/cmd/stop.go b/cmd/stop.go index c354f928..ac9816ee 100644 --- a/cmd/stop.go +++ b/cmd/stop.go @@ -1,9 +1,10 @@ package cmd import ( + "fmt" + "github.com/containers/podman-bootc/pkg/config" "github.com/containers/podman-bootc/pkg/user" - "github.com/containers/podman-bootc/pkg/utils" "github.com/containers/podman-bootc/pkg/vm" "github.com/sirupsen/logrus" @@ -23,28 +24,41 @@ func init() { } func doStop(_ *cobra.Command, args []string) (err error) { - user, err := user.NewUser() + usr, err := user.NewUser() if err != nil { return err } id := args[0] + fullImageId, err := usr.Storage().SearchByPrefix(id) + if err != nil { + return fmt.Errorf("searching for ID %s: %w", id, err) + } + if fullImageId == nil { + return fmt.Errorf("local installation '%s' does not exists", id) + } + + _, unlock, err := usr.Storage().GetExclusiveOrAdd(*fullImageId) + if err != nil { + return fmt.Errorf("unable to lock the VM cache: %w", err) + } + defer func() { + if err := unlock(); err != nil { + logrus.Errorf("unable to unlock VM %s: %v", id, err) + } + }() + bootcVM, err := vm.NewVM(vm.NewVMParameters{ - ImageID: id, + ImageID: string(*fullImageId), LibvirtUri: config.LibvirtUri, - User: user, - Locking: utils.Exclusive, + User: usr, }) if err != nil { return err } - // Let's be explicit instead of relying on the defer exec order defer func() { bootcVM.CloseConnection() - if err := bootcVM.Unlock(); err != nil { - logrus.Warningf("unable to unlock VM %s: %v", id, err) - } }() return bootcVM.Delete() diff --git a/pkg/bootc/bootc_disk.go b/pkg/bootc/bootc_disk.go index 75c36eda..0d9dc2a6 100644 --- a/pkg/bootc/bootc_disk.go +++ b/pkg/bootc/bootc_disk.go @@ -3,7 +3,6 @@ package bootc import ( "context" "encoding/json" - "errors" "fmt" "io" "os" @@ -14,8 +13,9 @@ import ( "time" "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/define" + "github.com/containers/podman-bootc/pkg/storage" "github.com/containers/podman-bootc/pkg/user" - "github.com/containers/podman-bootc/pkg/utils" "github.com/containers/podman/v5/pkg/bindings/containers" "github.com/containers/podman/v5/pkg/bindings/images" @@ -128,27 +128,20 @@ func (p *BootcDisk) Install(quiet bool, config DiskImageConfig) (err error) { } // Create VM cache dir; one per oci bootc image + // FIXME: This should be removed, as soon the storage/cache refactor is done p.Directory = filepath.Join(p.User.CacheDir(), p.ImageId) - lock := utils.NewCacheLock(p.User.RunDir(), p.Directory) - locked, err := lock.TryLock(utils.Exclusive) + + guard, unlock, err := p.User.Storage().GetExclusiveOrAdd(define.FullImageId(p.ImageId)) if err != nil { - return fmt.Errorf("error locking the VM cache path: %w", err) - } - if !locked { - return fmt.Errorf("unable to lock the VM cache path") + return fmt.Errorf("unable to lock the VM cache: %w", err) } - defer func() { - if err := lock.Unlock(); err != nil { + if err := unlock(); err != nil { logrus.Errorf("unable to unlock VM %s: %v", p.ImageId, err) } }() - if err := os.MkdirAll(p.Directory, os.ModePerm); err != nil { - return fmt.Errorf("error while making bootc disk directory: %w", err) - } - - err = p.getOrInstallImageToDisk(quiet, config) + err = p.getOrInstallImageToDisk(guard, quiet, config) if err != nil { return } @@ -172,16 +165,18 @@ func (p *BootcDisk) Cleanup() (err error) { } // getOrInstallImageToDisk checks if the disk is present and if not, installs the image to a new disk -func (p *BootcDisk) getOrInstallImageToDisk(quiet bool, diskConfig DiskImageConfig) error { - diskPath := filepath.Join(p.Directory, config.DiskImage) +func (p *BootcDisk) getOrInstallImageToDisk(guard *storage.WriteGuard, quiet bool, diskConfig DiskImageConfig) error { + diskPath, found := guard.FilePath(config.DiskImage) + if !found { + logrus.Debugf("No existing disk image found") + return p.bootcInstallImageToDisk(guard, quiet, diskConfig) + } + f, err := os.Open(diskPath) if err != nil { - if !errors.Is(err, os.ErrNotExist) { - return err - } - logrus.Debugf("No existing disk image found") - return p.bootcInstallImageToDisk(quiet, diskConfig) + return err } + logrus.Debug("Found existing disk image, comparing digest") defer f.Close() buf := make([]byte, 4096) @@ -190,13 +185,13 @@ func (p *BootcDisk) getOrInstallImageToDisk(quiet bool, diskConfig DiskImageConf // If there's no xattr, just remove it os.Remove(diskPath) logrus.Debugf("No %s xattr found", imageMetaXattr) - return p.bootcInstallImageToDisk(quiet, diskConfig) + return p.bootcInstallImageToDisk(guard, quiet, diskConfig) } bufTrimmed := buf[:len] var serializedMeta diskFromContainerMeta if err := json.Unmarshal(bufTrimmed, &serializedMeta); err != nil { logrus.Warnf("failed to parse serialized meta from %s (%v) %v", diskPath, buf, err) - return p.bootcInstallImageToDisk(quiet, diskConfig) + return p.bootcInstallImageToDisk(guard, quiet, diskConfig) } logrus.Debugf("previous disk digest: %s current digest: %s", serializedMeta.ImageDigest, p.ImageId) @@ -204,7 +199,7 @@ func (p *BootcDisk) getOrInstallImageToDisk(quiet bool, diskConfig DiskImageConf return nil } - return p.bootcInstallImageToDisk(quiet, diskConfig) + return p.bootcInstallImageToDisk(guard, quiet, diskConfig) } func align(size int64, align int64) int64 { @@ -216,7 +211,7 @@ func align(size int64, align int64) int64 { } // bootcInstallImageToDisk creates a disk image from a bootc container -func (p *BootcDisk) bootcInstallImageToDisk(quiet bool, diskConfig DiskImageConfig) (err error) { +func (p *BootcDisk) bootcInstallImageToDisk(guard *storage.WriteGuard, quiet bool, diskConfig DiskImageConfig) (err error) { fmt.Printf("Executing `bootc install to-disk` from container image %s to create disk image\n", p.RepoTag) p.file, err = os.CreateTemp(p.Directory, "podman-bootc-tempdisk") if err != nil { @@ -266,11 +261,11 @@ func (p *BootcDisk) bootcInstallImageToDisk(quiet bool, diskConfig DiskImageConf if err := unix.Fsetxattr(int(p.file.Fd()), imageMetaXattr, buf, 0); err != nil { return fmt.Errorf("failed to set xattr: %w", err) } - diskPath := filepath.Join(p.Directory, config.DiskImage) - if err := os.Rename(p.file.Name(), diskPath); err != nil { - return fmt.Errorf("failed to rename to %s: %w", diskPath, err) + if err := guard.MoveIntoRename(p.file.Name(), config.DiskImage); err != nil { + return err } + doCleanupDisk = false return nil diff --git a/pkg/define/types.go b/pkg/define/types.go new file mode 100644 index 00000000..98a955de --- /dev/null +++ b/pkg/define/types.go @@ -0,0 +1,3 @@ +package define + +type FullImageId string diff --git a/pkg/storage/bucket.go b/pkg/storage/bucket.go new file mode 100644 index 00000000..54d36509 --- /dev/null +++ b/pkg/storage/bucket.go @@ -0,0 +1,304 @@ +package storage + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/containers/podman-bootc/pkg/define" + + "github.com/gofrs/flock" + "github.com/sirupsen/logrus" +) + +var ErrInUse = errors.New("busy bucket") +var ErrFileNotFound = errors.New("file not found") + +type UnlockFunc func() error +type accessMode uint + +const invalidGuard = "invalid guard" +const ( + exclusive accessMode = iota + shared +) + +func NewBucket(cacheDir, runDir string) *Bucket { + return &Bucket{ + cacheDir: cacheDir, + runDir: runDir, + } +} + +type Bucket struct { + cacheDir string + runDir string +} + +func (p *Bucket) SearchByPrefix(prefix string) (*define.FullImageId, error) { + ids, err := p.List() + if err != nil { + return nil, err + } + + for _, cachedId := range ids { + if strings.HasPrefix(string(cachedId), prefix) { + return &cachedId, nil + } + } + + return nil, nil +} + +func (p *Bucket) List() ([]define.FullImageId, error) { + entries, err := os.ReadDir(p.cacheDir) + if err != nil { + return nil, err + } + + var ids []define.FullImageId + for _, e := range entries { + if e.IsDir() && len(e.Name()) == 64 { + ids = append(ids, define.FullImageId(e.Name())) + } + } + + return ids, nil +} + +// Get returns a read-only guard and its unlock function to the bucket +// or nil if it does not exist. If it's unable to acquire the lock it returns +// an error +func (p *Bucket) Get(id define.FullImageId) (*ReadOnlyGuard, UnlockFunc, error) { + // Before checking if the bucket exist let's acquire a lock to make sure + // that is not removed after checking it + lock, err := p.lock(id, shared) + if err != nil { + return nil, nil, err + } + + if !p.existsWithLock(id) { + if err := lock.Unlock(); err != nil { + logrus.Errorf("failed to unlock missing bucket: %s", id) + } + return nil, nil, nil + } + + bucket := p.bucketPath(id) + guard, unlock := makeROGuard(bucket, lock) + return guard, unlock, nil +} + +// GetExclusive returns a write guard and its unlock function to the bucket +// or nil if it does not exist. If it's unable to acquire the lock it returns +// an error +func (p *Bucket) GetExclusive(id define.FullImageId) (*WriteGuard, UnlockFunc, error) { + // Before checking if the bucket exist let's acquire a lock to make sure + // that is not removed after checking it + lock, err := p.lock(id, exclusive) + if err != nil { + return nil, nil, err + } + + if !p.existsWithLock(id) { + if err := lock.Unlock(); err != nil { + logrus.Errorf("failed to unlock missing bucket: %s", id) + } + return nil, nil, nil + } + + bucket := p.bucketPath(id) + guard, unlock := makeWGuard(bucket, lock) + return guard, unlock, nil +} + +// GetExclusiveOrAdd returns a write guard if the bucket exists, or it will create +// a new bucket if it does not exist returning a write guard. If it's unable +// to acquire the lock it returns an error +func (p *Bucket) GetExclusiveOrAdd(id define.FullImageId) (*WriteGuard, UnlockFunc, error) { + lock, err := p.lock(id, exclusive) + if err != nil { + return nil, nil, err + } + + bucket := p.bucketPath(id) + if !p.existsWithLock(id) { + if err := os.MkdirAll(bucket, os.ModePerm); err != nil { + if err := lock.Unlock(); err != nil { + logrus.Errorf("failed to unlock new bucket: %s", id) + } + return nil, nil, fmt.Errorf("error while making bucket directory: %w", err) + } + } + + guard, unlock := makeWGuard(bucket, lock) + return guard, unlock, nil +} + +func (p *Bucket) existsWithLock(id define.FullImageId) bool { + _, err := os.Stat(filepath.Join(p.cacheDir, string(id))) + return err == nil +} + +func (p *Bucket) lock(id define.FullImageId, mode accessMode) (*flock.Flock, error) { + lockFile := filepath.Join(p.runDir, string(id)+".lock") + lock := flock.New(lockFile) + + var locked bool + var err error + if mode == exclusive { + locked, err = lock.TryLock() + } else { + locked, err = lock.TryRLock() + } + + if err != nil { + return nil, err + } + + if !locked { + return nil, ErrInUse + } + + return lock, nil +} + +func (p *Bucket) bucketPath(id define.FullImageId) string { + return filepath.Join(p.cacheDir, string(id)) +} + +func makeROGuard(bucket string, lock *flock.Flock) (*ReadOnlyGuard, UnlockFunc) { + guard := &ReadOnlyGuard{ + lock: lock, + path: bucket, + locked: true, + } + unlock := &unlockGuard{ + lock: lock, + guard: guard, + } + return guard, func() error { return unlock.unlock() } +} + +type ReadOnlyGuard struct { + lock *flock.Flock + path string + locked bool +} + +func (p *ReadOnlyGuard) Load(fileName string) ([]byte, error) { + p.lockGuard() + + fullPath, found := checkAndGetFullPath(p.path, fileName) + if !found { + return nil, fmt.Errorf("loading file: %w", ErrFileNotFound) + } + + data, err := os.ReadFile(fullPath) + + if err != nil { + return nil, fmt.Errorf("loading file: %w", err) + } + return data, nil +} + +func (p *ReadOnlyGuard) lockGuard() { + if !p.locked { + panic(invalidGuard) + } +} + +func (p *ReadOnlyGuard) unlocked() { + p.locked = false +} + +func makeWGuard(bucket string, lock *flock.Flock) (*WriteGuard, UnlockFunc) { + guard := &WriteGuard{ + lock: lock, + path: bucket, + locked: true, + } + unlock := &unlockGuard{ + lock: lock, + guard: guard, + } + return guard, func() error { return unlock.unlock() } +} + +type WriteGuard struct { + lock *flock.Flock + path string + locked bool +} + +// FilePath returns the path to a file (possibly) stored in the bucket, +// and a boolean indicating if it's present or not. +func (p *WriteGuard) FilePath(fileName string) (string, bool) { + p.lockGuard() + + return checkAndGetFullPath(p.path, fileName) +} + +func (p *WriteGuard) Store(fileName string, data []byte) error { + p.lockGuard() + + fullPath := filepath.Join(p.path, fileName) + err := os.WriteFile(fullPath, data, 0660) + if err != nil { + return fmt.Errorf("storing file: %w", err) + } + return nil +} + +// MoveIntoRename imports (moves) fileName to the bucket with a provided name. +// If fileName already exists inside the bucket, MoveInto replaces it. +func (p *WriteGuard) MoveIntoRename(srcFullPath, newName string) error { + p.lockGuard() + + dstFullPath := filepath.Join(p.path, newName) + + // this could fail with "invalid cross-device link", if src and destination + // are on different devices. In our use case this is not relevant, since we + // expect the scratch and storage space will be in the same device + if err := os.Rename(srcFullPath, dstFullPath); err != nil { + return fmt.Errorf("importing file: failed to move %s to %s: %w", srcFullPath, newName, err) + } + return nil +} + +func (p *WriteGuard) Remove() error { + p.lockGuard() + return os.RemoveAll(p.path) +} + +func (p *WriteGuard) lockGuard() { + if !p.locked { + panic(invalidGuard) + } +} + +func (p *WriteGuard) unlocked() { + p.locked = false +} + +type guardApi interface { + unlocked() +} + +type unlockGuard struct { + lock *flock.Flock + guard guardApi +} + +func (p *unlockGuard) unlock() error { + p.guard.unlocked() + return p.lock.Unlock() +} + +func checkAndGetFullPath(path, fileName string) (string, bool) { + fullPath := filepath.Join(path, fileName) + _, err := os.Stat(fullPath) + return fullPath, err == nil +} diff --git a/pkg/storage/bucket_test.go b/pkg/storage/bucket_test.go new file mode 100644 index 00000000..4cd202f3 --- /dev/null +++ b/pkg/storage/bucket_test.go @@ -0,0 +1,337 @@ +package storage_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/define" + "github.com/containers/podman-bootc/pkg/storage" + + "github.com/adrg/xdg" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCacheBucket(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Cache Bucket Suite") +} + +const FakeFullId = define.FullImageId("0000000000000000000000000000000000000000000000000000000000000000") // Just 64 0's +const projectTest = config.ProjectName + "-test" + +var baseDir = filepath.Join(xdg.RuntimeDir, projectTest) +var cacheDir = filepath.Join(baseDir, "cache") +var runDir = filepath.Join(baseDir, "run") +var scratchDir = filepath.Join(baseDir, "scratch") + +var _ = BeforeEach(func() { + err := os.MkdirAll(cacheDir, 0700) + Expect(err).To(Not(HaveOccurred())) + + err = os.MkdirAll(runDir, 0700) + Expect(err).To(Not(HaveOccurred())) + + err = os.MkdirAll(scratchDir, 0700) + Expect(err).To(Not(HaveOccurred())) +}) + +var _ = AfterEach(func() { + err := os.RemoveAll(baseDir) + Expect(err).To(Not(HaveOccurred())) +}) + +var _ = Describe("Bucket", func() { + bucket := storage.NewBucket(cacheDir, runDir) + + Context("does not exist", func() { + It("should return an empty slice", func() { + list, err := bucket.List() + Expect(err).To(Not(HaveOccurred())) + Expect(list).Should(BeEmpty()) + }) + + It("should return a nil ptr", func() { + id, err := bucket.SearchByPrefix("0") + Expect(err).To(Not(HaveOccurred())) + Expect(id).Should(BeNil()) + + // Asking for shared access + roguard, rounlock, err := bucket.Get(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(roguard).Should(BeNil()) + Expect(rounlock).Should(BeNil()) + + // Asking for exclusive access + wguard, wunlock, err := bucket.GetExclusive(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(wguard).Should(BeNil()) + Expect(wunlock).Should(BeNil()) + }) + + It("should create a new entry", func() { + wguard, wunlock, err := bucket.GetExclusiveOrAdd(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(wguard).Should(Not(BeNil())) + Expect(wunlock).Should(Not(BeNil())) + + err = wunlock() + Expect(err).To(Not(HaveOccurred())) + + list, err := bucket.List() + Expect(err).To(Not(HaveOccurred())) + Expect(list).Should(Not(BeEmpty())) + + // Let's search for it + id, err := bucket.SearchByPrefix("0") + Expect(err).To(Not(HaveOccurred())) + Expect(id).Should(Not(BeNil())) + Expect(*id).Should(Equal(FakeFullId)) + + // Asking for shared access + roguard, rounlock, err := bucket.Get(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(roguard).Should(Not(BeNil())) + Expect(rounlock).Should(Not(BeNil())) + + err = rounlock() + Expect(err).To(Not(HaveOccurred())) + + // Asking for exclusive access + wguard, wunlock, err = bucket.GetExclusive(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(wguard).Should(Not(BeNil())) + Expect(wunlock).Should(Not(BeNil())) + + err = wunlock() + Expect(err).To(Not(HaveOccurred())) + }) + }) + + Context("does exist with an exclusive access hold", func() { + var guard *storage.WriteGuard + var unlock storage.UnlockFunc + var err error + BeforeEach(func() { + guard, unlock, err = bucket.GetExclusiveOrAdd(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(guard).Should(Not(BeNil())) + Expect(unlock).Should(Not(BeNil())) + }) + + AfterEach(func() { + err := unlock() + Expect(err).To(Not(HaveOccurred())) + }) + + It("should return an non-empty slice", func() { + list, err := bucket.List() + Expect(err).To(Not(HaveOccurred())) + Expect(list).Should(Not(BeEmpty())) + }) + + It("should return a non-nil ptr", func() { + id, err := bucket.SearchByPrefix("0") + Expect(err).To(Not(HaveOccurred())) + Expect(id).Should(Not(BeNil())) + Expect(*id).Should(Equal(FakeFullId)) + }) + + It("should return an error", func() { + wguard, wunlock, err := bucket.GetExclusiveOrAdd(FakeFullId) + Expect(err).To(HaveOccurred()) + Expect(wguard).Should(BeNil()) + Expect(wunlock).Should(BeNil()) + + // Asking for shared access + roguard, rounlock, err := bucket.Get(FakeFullId) + Expect(err).To(HaveOccurred()) + Expect(roguard).Should(BeNil()) + Expect(rounlock).Should(BeNil()) + + // Asking for exclusive access + wguard, wunlock, err = bucket.GetExclusive(FakeFullId) + Expect(err).To(HaveOccurred()) + Expect(wguard).Should(BeNil()) + Expect(wunlock).Should(BeNil()) + }) + }) + + Context("does exist with a shared access hold", func() { + var guard *storage.ReadOnlyGuard + var unlock storage.UnlockFunc + + BeforeEach(func() { + wguard, wunlock, err := bucket.GetExclusiveOrAdd(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(wguard).Should(Not(BeNil())) + Expect(wunlock).Should(Not(BeNil())) + _ = wunlock() + + guard, unlock, err = bucket.Get(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(guard).Should(Not(BeNil())) + Expect(unlock).Should(Not(BeNil())) + }) + + AfterEach(func() { + err := unlock() + Expect(err).To(Not(HaveOccurred())) + }) + + It("should return an non-empty slice", func() { + list, err := bucket.List() + Expect(err).To(Not(HaveOccurred())) + Expect(list).Should(Not(BeEmpty())) + }) + + It("should return a non-nil ptr", func() { + id, err := bucket.SearchByPrefix("0") + Expect(err).To(Not(HaveOccurred())) + Expect(id).Should(Not(BeNil())) + Expect(*id).Should(Equal(FakeFullId)) + }) + + It("should return an error", func() { + wguard, wunlock, err := bucket.GetExclusiveOrAdd(FakeFullId) + Expect(err).To(HaveOccurred()) + Expect(wguard).Should(BeNil()) + Expect(wunlock).Should(BeNil()) + + wguard, wunlock, err = bucket.GetExclusive(FakeFullId) + Expect(err).To(HaveOccurred()) + Expect(wguard).Should(BeNil()) + Expect(wunlock).Should(BeNil()) + }) + + It("should succeed", func() { + roguard, rounlock, err := bucket.Get(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(roguard).Should(Not(BeNil())) + Expect(rounlock).Should(Not(BeNil())) + + err = rounlock() + Expect(err).To(Not(HaveOccurred())) + }) + }) + + Context("an exclusive access hold", func() { + It("should create a new entry and remove it", func() { + // it should not be present + id, err := bucket.SearchByPrefix("0") + Expect(err).To(Not(HaveOccurred())) + Expect(id).Should(BeNil()) + + // create a new entry + guard, unlock, err := bucket.GetExclusiveOrAdd(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(guard).Should(Not(BeNil())) + Expect(unlock).Should(Not(BeNil())) + + // Let's search for the new entry + id, err = bucket.SearchByPrefix("0") + Expect(err).To(Not(HaveOccurred())) + Expect(id).Should(Not(BeNil())) + Expect(*id).Should(Equal(FakeFullId)) + + // remove the entry + err = guard.Remove() + Expect(err).To(Not(HaveOccurred())) + + // it should not be present + id, err = bucket.SearchByPrefix("0") + Expect(err).To(Not(HaveOccurred())) + Expect(id).Should(BeNil()) + + err = unlock() + Expect(err).To(Not(HaveOccurred())) + }) + }) + + Context("storing and extracting data", func() { + const testFile = "config.dat" + const tmpFile = "import.dat" + + var testData = []byte("Hello, Test") + It("should store a file and recover its content", func() { + guard, unlock, err := bucket.GetExclusiveOrAdd(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(guard).Should(Not(BeNil())) + Expect(unlock).Should(Not(BeNil())) + + err = guard.Store(testFile, testData) + Expect(err).To(Not(HaveOccurred())) + + err = unlock() + Expect(err).To(Not(HaveOccurred())) + + roguard, rounlock, err := bucket.Get(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(roguard).Should(Not(BeNil())) + Expect(rounlock).Should(Not(BeNil())) + + data, err := roguard.Load(testFile) + Expect(err).To(Not(HaveOccurred())) + Expect(data).To(Equal(testData)) + + err = rounlock() + Expect(err).To(Not(HaveOccurred())) + }) + + It("should import a file", func() { + // let's create a simple file + f := filepath.Join(scratchDir, tmpFile) + err := os.WriteFile(f, []byte("xxx"), 0660) + Expect(err).To(Not(HaveOccurred())) + + // Import + guard, unlock, err := bucket.GetExclusiveOrAdd(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(guard).Should(Not(BeNil())) + Expect(unlock).Should(Not(BeNil())) + + err = guard.MoveIntoRename(f, tmpFile) + Expect(err).To(Not(HaveOccurred())) + + // the file should be moved into the storage + _, err = os.Stat(f) + Expect(err).To(HaveOccurred()) + + _, err = os.Stat(filepath.Join(cacheDir, string(FakeFullId), tmpFile)) + Expect(err).To(Not(HaveOccurred())) + + err = unlock() + Expect(err).To(Not(HaveOccurred())) + }) + }) + + Context("invalid guard", func() { + It("should panic", func() { + // Exclusive guard + guard, unlock, err := bucket.GetExclusiveOrAdd(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(guard).Should(Not(BeNil())) + Expect(unlock).Should(Not(BeNil())) + + err = unlock() + Expect(err).To(Not(HaveOccurred())) + + Expect(func() { _, _ = guard.FilePath("xxx") }).To(Panic()) + Expect(func() { _ = guard.Store("yyy", []byte("xxx")) }).To(Panic()) + Expect(func() { _ = guard.MoveIntoRename("src", "dst") }).To(Panic()) + Expect(func() { _ = guard.Remove() }).To(Panic()) + + // Shared guard + roguard, rounlock, err := bucket.Get(FakeFullId) + Expect(err).To(Not(HaveOccurred())) + Expect(roguard).Should(Not(BeNil())) + Expect(unlock).Should(Not(BeNil())) + + err = rounlock() + Expect(err).To(Not(HaveOccurred())) + Expect(func() { _, _ = roguard.Load("yyy") }).To(Panic()) + }) + }) +}) diff --git a/pkg/user/user.go b/pkg/user/user.go index 6d622a37..e79acb9f 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/storage" "github.com/adrg/xdg" "github.com/containers/podman/v5/pkg/rootless" @@ -15,6 +16,7 @@ import ( type User struct { OSUser *user.User + Stor *storage.Bucket } func NewUser() (u User, err error) { @@ -31,8 +33,12 @@ func NewUser() (u User, err error) { return u, fmt.Errorf("failed to get user: %w", err) } + cacheDir := filepath.Join(osUser.HomeDir, config.CacheDir, config.ProjectName) + runDir := filepath.Join(xdg.RuntimeDir, config.ProjectName, "run") + return User{ OSUser: osUser, + Stor: storage.NewBucket(cacheDir, runDir), }, nil } @@ -52,6 +58,10 @@ func (u *User) CacheDir() string { return filepath.Join(u.HomeDir(), config.CacheDir, config.ProjectName) } +func (u *User) Storage() *storage.Bucket { + return u.Stor +} + func (u *User) DefaultIdentity() string { return filepath.Join(u.SSHDir(), "id_rsa") } diff --git a/pkg/utils/locks.go b/pkg/utils/locks.go deleted file mode 100644 index e5c22657..00000000 --- a/pkg/utils/locks.go +++ /dev/null @@ -1,41 +0,0 @@ -package utils - -import ( - "path/filepath" - - "github.com/gofrs/flock" -) - -type AccessMode uint - -const ( - Exclusive AccessMode = iota - Shared -) - -type CacheLock struct { - inner *flock.Flock -} - -// NewCacheLock returns a new instance of *CacheLock. It takes the path to the VM cache dir. -func NewCacheLock(lockDir, cacheDir string) CacheLock { - imageLongID := filepath.Base(cacheDir) - cacheDirLockFile := filepath.Join(lockDir, imageLongID+".lock") - return CacheLock{inner: flock.New(cacheDirLockFile)} -} - -// TryLock takes an exclusive or shared lock, based on the parameter mode. -// The lock is non-blocking, if we are unable to lock the cache directory, -// the function will return false instead of waiting for the lock. -func (l CacheLock) TryLock(mode AccessMode) (bool, error) { - if mode == Exclusive { - return l.inner.TryLock() - } else { - return l.inner.TryRLock() - } -} - -// Unlock unlocks the cache lock. -func (l CacheLock) Unlock() error { - return l.inner.Unlock() -} diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 9e1c2c99..5903370b 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -9,14 +9,12 @@ import ( "os/exec" "path/filepath" "strconv" - "strings" "time" "github.com/containers/podman-bootc/pkg/bootc" "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/storage" "github.com/containers/podman-bootc/pkg/user" - "github.com/containers/podman-bootc/pkg/utils" - "github.com/docker/go-units" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" @@ -26,30 +24,13 @@ var ErrVMInUse = errors.New("VM already in use") // GetVMCachePath returns the path to the VM cache directory func GetVMCachePath(imageId string, user user.User) (longID string, path string, err error) { - files, err := os.ReadDir(user.CacheDir()) - if err != nil { - return "", "", err - } - - fullImageId := "" - for _, f := range files { - if f.IsDir() && len(f.Name()) == 64 && strings.HasPrefix(f.Name(), imageId) { - fullImageId = f.Name() - } - } - - if fullImageId == "" { - return "", "", fmt.Errorf("local installation '%s' does not exists", imageId) - } - - return fullImageId, filepath.Join(user.CacheDir(), fullImageId), nil + return imageId, filepath.Join(user.CacheDir(), imageId), nil } type NewVMParameters struct { ImageID string User user.User //user who is running the podman bootc command LibvirtUri string //linux only - Locking utils.AccessMode } type RunVMParameters struct { @@ -70,13 +51,12 @@ type BootcVM interface { IsRunning() (bool, error) WriteConfig(bootc.BootcDisk) error WaitForSSHToBeReady() error - RunSSH([]string) error + RunSSH(*storage.ReadOnlyGuard, []string) error DeleteFromCache() error Exists() (bool, error) - GetConfig() (*BootcVMConfig, error) + GetConfig(*storage.ReadOnlyGuard) (*BootcVMConfig, error) CloseConnection() PrintConsole() error - Unlock() error } type BootcVMCommon struct { @@ -95,7 +75,6 @@ type BootcVMCommon struct { hasCloudInit bool cloudInitDir string cloudInitArgs string - cacheDirLock utils.CacheLock } type BootcVMConfig struct { @@ -136,9 +115,8 @@ func (v *BootcVMCommon) WriteConfig(bootcDisk bootc.BootcDisk) error { } -func (v *BootcVMCommon) LoadConfigFile() (cfg *BootcVMConfig, err error) { - cfgFile := filepath.Join(v.cacheDir, config.CfgFile) - fileContent, err := os.ReadFile(cfgFile) +func (v *BootcVMCommon) LoadConfigFile(guard *storage.ReadOnlyGuard) (cfg *BootcVMConfig, err error) { + fileContent, err := guard.Load(config.CfgFile) if err != nil { return } @@ -213,8 +191,8 @@ func (v *BootcVMCommon) WaitForSSHToBeReady() error { } // RunSSH runs a command over ssh or starts an interactive ssh connection if no command is provided -func (v *BootcVMCommon) RunSSH(inputArgs []string) error { - cfg, err := v.LoadConfigFile() +func (v *BootcVMCommon) RunSSH(guard *storage.ReadOnlyGuard, inputArgs []string) error { + cfg, err := v.LoadConfigFile(guard) if err != nil { return fmt.Errorf("failed to load VM config: %w", err) } @@ -280,31 +258,3 @@ func (b *BootcVMCommon) tmpFileInjectSshKeyEnc() (string, error) { tmpFileCmdEnc := base64.StdEncoding.EncodeToString([]byte(tmpFileCmd)) return tmpFileCmdEnc, nil } - -func lockVM(params NewVMParameters, cacheDir string) (utils.CacheLock, error) { - lock := utils.NewCacheLock(params.User.RunDir(), cacheDir) - locked, err := lock.TryLock(params.Locking) - if err != nil { - return lock, fmt.Errorf("unable to lock the VM cache path: %w", err) - } - - if !locked { - return lock, ErrVMInUse - } - - cacheDirExists, err := utils.FileExists(cacheDir) - if err != nil { - if err := lock.Unlock(); err != nil { - logrus.Debugf("unlock failed: %v", err) - } - return lock, fmt.Errorf("unable to check cache path: %w", err) - } - if !cacheDirExists { - if err := lock.Unlock(); err != nil { - logrus.Debugf("unlock failed: %v", err) - } - return lock, fmt.Errorf("'%s' does not exists", params.ImageID) - } - - return lock, nil -} diff --git a/pkg/vm/vm_darwin.go b/pkg/vm/vm_darwin.go index 3f278d01..ce78513a 100644 --- a/pkg/vm/vm_darwin.go +++ b/pkg/vm/vm_darwin.go @@ -10,6 +10,7 @@ import ( "time" "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/storage" "github.com/containers/podman-bootc/pkg/utils" "github.com/sirupsen/logrus" @@ -30,11 +31,6 @@ func NewVM(params NewVMParameters) (vm *BootcVMMac, err error) { return nil, fmt.Errorf("unable to get VM cache path: %w", err) } - lock, err := lockVM(params, cacheDir) - if err != nil { - return nil, err - } - vm = &BootcVMMac{ socketFile: filepath.Join(params.User.CacheDir(), longId[:12]+"-console.sock"), BootcVMCommon: BootcVMCommon{ @@ -48,7 +44,6 @@ func NewVM(params NewVMParameters) (vm *BootcVMMac, err error) { } return vm, nil - } func (b *BootcVMMac) CloseConnection() { @@ -83,8 +78,8 @@ func (b *BootcVMMac) PrintConsole() (err error) { } } -func (b *BootcVMMac) GetConfig() (cfg *BootcVMConfig, err error) { - cfg, err = b.LoadConfigFile() +func (b *BootcVMMac) GetConfig(guard *storage.ReadOnlyGuard) (cfg *BootcVMConfig, err error) { + cfg, err = b.LoadConfigFile(guard) if err != nil { return } @@ -244,7 +239,3 @@ func getQemuInstallPath() (string, error) { return "", errors.New("QEMU binary not found") } - -func (v *BootcVMMac) Unlock() error { - return v.cacheDirLock.Unlock() -} diff --git a/pkg/vm/vm_linux.go b/pkg/vm/vm_linux.go index 72e61df8..f8575413 100644 --- a/pkg/vm/vm_linux.go +++ b/pkg/vm/vm_linux.go @@ -11,6 +11,7 @@ import ( "time" "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/storage" "github.com/sirupsen/logrus" "libvirt.org/go/libvirt" @@ -44,11 +45,6 @@ func NewVM(params NewVMParameters) (vm *BootcVMLinux, err error) { return nil, fmt.Errorf("unable to get VM cache path: %w", err) } - lock, err := lockVM(params, cacheDir) - if err != nil { - return nil, err - } - vm = &BootcVMLinux{ libvirtUri: params.LibvirtUri, BootcVMCommon: BootcVMCommon{ @@ -57,23 +53,19 @@ func NewVM(params NewVMParameters) (vm *BootcVMLinux, err error) { cacheDir: cacheDir, diskImagePath: filepath.Join(cacheDir, config.DiskImage), user: params.User, - cacheDirLock: lock, }, } err = vm.loadExistingDomain() if err != nil { - if err := vm.Unlock(); err != nil { - logrus.Debugf("unlock failed: %v", err) - } return vm, fmt.Errorf("unable to load existing libvirt domain: %w", err) } return vm, nil } -func (v *BootcVMLinux) GetConfig() (cfg *BootcVMConfig, err error) { - cfg, err = v.LoadConfigFile() +func (v *BootcVMLinux) GetConfig(guard *storage.ReadOnlyGuard) (cfg *BootcVMConfig, err error) { + cfg, err = v.LoadConfigFile(guard) if err != nil { return } @@ -287,16 +279,6 @@ func (v *BootcVMLinux) loadExistingDomain() (err error) { } } - // if domain exists, load it's config - if v.domain != nil { - cfg, err := v.GetConfig() - if err != nil { - return fmt.Errorf("unable to load VM config: %w", err) - } - v.sshPort = cfg.SshPort - v.sshIdentity = cfg.SshIdentity - } - return nil } @@ -380,7 +362,3 @@ func (v *BootcVMLinux) IsRunning() (exists bool, err error) { return false, nil } } - -func (v *BootcVMLinux) Unlock() error { - return v.cacheDirLock.Unlock() -} diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 767f8489..0745078e 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -4,6 +4,7 @@ package vm_test import ( "context" + "github.com/adrg/xdg" "os" osUser "os/user" "path/filepath" @@ -12,8 +13,9 @@ import ( "github.com/containers/podman-bootc/cmd" "github.com/containers/podman-bootc/pkg/bootc" + "github.com/containers/podman-bootc/pkg/config" + "github.com/containers/podman-bootc/pkg/storage" "github.com/containers/podman-bootc/pkg/user" - "github.com/containers/podman-bootc/pkg/utils" "github.com/containers/podman-bootc/pkg/vm" . "github.com/onsi/ginkgo/v2" @@ -35,15 +37,22 @@ func projectRoot() string { return projectRoot } -var testUser = user.User{ - OSUser: &osUser.User{ - Uid: "1000", - Gid: "1000", - Username: "test", - Name: "test", - HomeDir: filepath.Join(projectRoot(), ".test-user-home"), - }, -} +var ( + homeDir = filepath.Join(projectRoot(), ".test-user-home") + cacheDir = filepath.Join(homeDir, config.CacheDir, config.ProjectName) + runDir = filepath.Join(xdg.RuntimeDir, config.ProjectName, "run") + + testUser = user.User{ + OSUser: &osUser.User{ + Uid: "1000", + Gid: "1000", + Username: "test", + Name: "test", + HomeDir: homeDir, + }, + Stor: storage.NewBucket(cacheDir, runDir), + } +) const ( testImageID = "a025064b145ed339eeef86046aea3ee221a2a5a16f588aff4f43a42e5ca9f844" @@ -85,7 +94,6 @@ func createTestVM(imageId string) (bootcVM *vm.BootcVMLinux) { ImageID: imageId, User: testUser, LibvirtUri: testLibvirtUri, - Locking: utils.Shared, }) Expect(err).To(Not(HaveOccurred())) @@ -156,9 +164,6 @@ var _ = Describe("VM", func() { Context("does not exist", func() { It("should create and start the VM after calling Run", func() { bootcVM := createTestVM(testImageID) - defer func() { - _ = bootcVM.Unlock() - }() runTestVM(bootcVM) exists, err := bootcVM.Exists() @@ -172,9 +177,6 @@ var _ = Describe("VM", func() { It("should return false when calling Exists before Run", func() { bootcVM := createTestVM(testImageID) - defer func() { - _ = bootcVM.Unlock() - }() exists, err := bootcVM.Exists() Expect(err).To(Not(HaveOccurred())) @@ -192,9 +194,6 @@ var _ = Describe("VM", func() { It("should remove the VM from the hypervisor after calling Delete", func() { //create vm and start it bootcVM := createTestVM(testImageID) - defer func() { - _ = bootcVM.Unlock() - }() runTestVM(bootcVM) @@ -215,9 +214,6 @@ var _ = Describe("VM", func() { It("should list the VM", func() { bootcVM := createTestVM(testImageID) - defer func() { - _ = bootcVM.Unlock() - }() runTestVM(bootcVM) vmList, err := cmd.CollectVmList(testUser, testLibvirtUri) @@ -239,25 +235,16 @@ var _ = Describe("VM", func() { Context("multiple running", func() { It("should list all VMs", func() { bootcVM := createTestVM(testImageID) - defer func() { - _ = bootcVM.Unlock() - }() runTestVM(bootcVM) id2 := "1234564b145ed339eeef86046aea3ee221a2a5a16f588aff4f43a42e5ca9f844" bootcVM2 := createTestVM(id2) - defer func() { - _ = bootcVM2.Unlock() - }() runTestVM(bootcVM2) id3 := "2345674b145ed339eeef86046aea3ee221a2a5a16f588aff4f43a42e5ca9f844" bootcVM3 := createTestVM(id3) - defer func() { - _ = bootcVM3.Unlock() - }() runTestVM(bootcVM3)