devmapper: add pool metadata
Signed-off-by: Maksym Pavlenko <makpav@amazon.com>
This commit is contained in:
parent
809e5fd3b8
commit
fcd9dc2749
104
snapshots/devmapper/device_info.go
Normal file
104
snapshots/devmapper/device_info.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package devmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxDeviceID = 0xffffff // Device IDs are 24-bit numbers
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeviceState represents current devmapper device state reflected in meta store
|
||||||
|
type DeviceState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Unknown means that device just allocated and no operations were performed
|
||||||
|
Unknown DeviceState = iota
|
||||||
|
// Creating means that device is going to be created
|
||||||
|
Creating
|
||||||
|
// Created means that devices successfully created
|
||||||
|
Created
|
||||||
|
// Activating means that device is going to be activated
|
||||||
|
Activating
|
||||||
|
// Activated means that device successfully activated
|
||||||
|
Activated
|
||||||
|
// Suspending means that device is going to be suspended
|
||||||
|
Suspending
|
||||||
|
// Suspended means that device successfully suspended
|
||||||
|
Suspended
|
||||||
|
// Resuming means that device is going to be resumed from suspended state
|
||||||
|
Resuming
|
||||||
|
// Resumed means that device successfully resumed
|
||||||
|
Resumed
|
||||||
|
// Deactivating means that device is going to be deactivated
|
||||||
|
Deactivating
|
||||||
|
// Deactivated means that device successfully deactivated
|
||||||
|
Deactivated
|
||||||
|
// Removing means that device is going to be removed
|
||||||
|
Removing
|
||||||
|
// Removed means that device successfully removed but not yet deleted from meta store
|
||||||
|
Removed
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s DeviceState) String() string {
|
||||||
|
switch s {
|
||||||
|
case Creating:
|
||||||
|
return "Creating"
|
||||||
|
case Created:
|
||||||
|
return "Created"
|
||||||
|
case Activating:
|
||||||
|
return "Activating"
|
||||||
|
case Activated:
|
||||||
|
return "Activated"
|
||||||
|
case Suspending:
|
||||||
|
return "Suspending"
|
||||||
|
case Suspended:
|
||||||
|
return "Suspended"
|
||||||
|
case Resuming:
|
||||||
|
return "Resuming"
|
||||||
|
case Resumed:
|
||||||
|
return "Resumed"
|
||||||
|
case Deactivating:
|
||||||
|
return "Deactivating"
|
||||||
|
case Deactivated:
|
||||||
|
return "Deactivated"
|
||||||
|
case Removing:
|
||||||
|
return "Removing"
|
||||||
|
case Removed:
|
||||||
|
return "Removed"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("unknown %d", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceInfo represents metadata for thin device within thin-pool
|
||||||
|
type DeviceInfo struct {
|
||||||
|
// DeviceID is a 24-bit number assigned to a device within thin-pool device
|
||||||
|
DeviceID uint32 `json:"device_id"`
|
||||||
|
// Size is a thin device size
|
||||||
|
Size uint64 `json:"size"`
|
||||||
|
// Name is a device name to be used in /dev/mapper/
|
||||||
|
Name string `json:"name"`
|
||||||
|
// ParentName is a name of parent device (if snapshot)
|
||||||
|
ParentName string `json:"parent_name"`
|
||||||
|
// State represents current device state
|
||||||
|
State DeviceState `json:"state"`
|
||||||
|
// Error details if device state change failed
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
313
snapshots/devmapper/metadata.go
Normal file
313
snapshots/devmapper/metadata.go
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package devmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// DeviceInfoCallback is a callback used for device updates
|
||||||
|
DeviceInfoCallback func(deviceInfo *DeviceInfo) error
|
||||||
|
)
|
||||||
|
|
||||||
|
type deviceIDState byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
deviceFree deviceIDState = iota
|
||||||
|
deviceTaken
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bucket names
|
||||||
|
var (
|
||||||
|
devicesBucketName = []byte("devices") // Contains thin devices metadata <device_name>=<DeviceInfo>
|
||||||
|
deviceIDBucketName = []byte("device_ids") // Tracks used device ids <device_id_[0..maxDeviceID)>=<byte_[0/1]>
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNotFound represents an error returned when object not found in meta store
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
// ErrAlreadyExists represents an error returned when object can't be duplicated in meta store
|
||||||
|
ErrAlreadyExists = errors.New("object already exists")
|
||||||
|
)
|
||||||
|
|
||||||
|
// PoolMetadata keeps device info for the given thin-pool device, it also responsible for
|
||||||
|
// generating next available device ids and tracking devmapper transaction numbers
|
||||||
|
type PoolMetadata struct {
|
||||||
|
db *bolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPoolMetadata creates new or open existing pool metadata database
|
||||||
|
func NewPoolMetadata(dbfile string) (*PoolMetadata, error) {
|
||||||
|
db, err := bolt.Open(dbfile, 0600, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &PoolMetadata{db: db}
|
||||||
|
if err := metadata.ensureDatabaseInitialized(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to initialize database")
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDatabaseInitialized creates buckets required for metadata store in order
|
||||||
|
// to avoid bucket existence checks across the code
|
||||||
|
func (m *PoolMetadata) ensureDatabaseInitialized() error {
|
||||||
|
return m.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(devicesBucketName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(deviceIDBucketName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDevice saves device info to database.
|
||||||
|
func (m *PoolMetadata) AddDevice(ctx context.Context, info *DeviceInfo) error {
|
||||||
|
return m.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
devicesBucket := tx.Bucket(devicesBucketName)
|
||||||
|
|
||||||
|
// Make sure device name is unique
|
||||||
|
if err := getObject(devicesBucket, info.Name, nil); err == nil {
|
||||||
|
return ErrAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find next available device ID
|
||||||
|
deviceID, err := getNextDeviceID(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
info.DeviceID = deviceID
|
||||||
|
|
||||||
|
return putObject(devicesBucket, info.Name, info, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNextDeviceID finds the next free device ID by taking a cursor
|
||||||
|
// through the deviceIDBucketName bucket and finding the next sequentially
|
||||||
|
// unassigned ID. Device ID state is marked by a byte deviceFree or
|
||||||
|
// deviceTaken. Low device IDs will be reused sooner.
|
||||||
|
func getNextDeviceID(tx *bolt.Tx) (uint32, error) {
|
||||||
|
bucket := tx.Bucket(deviceIDBucketName)
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
|
||||||
|
// Check if any device id can be reused.
|
||||||
|
// Bolt stores its keys in byte-sorted order within a bucket.
|
||||||
|
// This makes sequential iteration extremely fast.
|
||||||
|
for key, taken := cursor.First(); key != nil; key, taken = cursor.Next() {
|
||||||
|
isFree := taken[0] == byte(deviceFree)
|
||||||
|
if !isFree {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedID, err := strconv.ParseUint(string(key), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id := uint32(parsedID)
|
||||||
|
if err := markDeviceID(tx, id, deviceTaken); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try allocate new device ID
|
||||||
|
seq, err := bucket.NextSequence()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if seq >= maxDeviceID {
|
||||||
|
return 0, errors.Errorf("dm-meta: couldn't find free device key")
|
||||||
|
}
|
||||||
|
|
||||||
|
id := uint32(seq)
|
||||||
|
if err := markDeviceID(tx, id, deviceTaken); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// markDeviceID marks a device as deviceFree or deviceTaken
|
||||||
|
func markDeviceID(tx *bolt.Tx, deviceID uint32, state deviceIDState) error {
|
||||||
|
var (
|
||||||
|
bucket = tx.Bucket(deviceIDBucketName)
|
||||||
|
key = strconv.FormatUint(uint64(deviceID), 10)
|
||||||
|
value = []byte{byte(state)}
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := bucket.Put([]byte(key), value); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to free device id %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDevice updates device info in metadata store.
|
||||||
|
// The callback should be used to indicate whether device info update was successful or not.
|
||||||
|
// An error returned from the callback will rollback the update transaction in the database.
|
||||||
|
// Name and Device ID are not allowed to change.
|
||||||
|
func (m *PoolMetadata) UpdateDevice(ctx context.Context, name string, fn DeviceInfoCallback) error {
|
||||||
|
return m.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
var (
|
||||||
|
device = &DeviceInfo{}
|
||||||
|
bucket = tx.Bucket(devicesBucketName)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := getObject(bucket, name, device); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow changing these values, keep things in sync with devmapper
|
||||||
|
name := device.Name
|
||||||
|
devID := device.DeviceID
|
||||||
|
|
||||||
|
if err := fn(device); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if name != device.Name {
|
||||||
|
return fmt.Errorf("failed to update device info, name didn't match: %q %q", name, device.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if devID != device.DeviceID {
|
||||||
|
return fmt.Errorf("failed to update device info, device id didn't match: %d %d", devID, device.DeviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return putObject(bucket, name, device, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDevice retrieves device info by name from database
|
||||||
|
func (m *PoolMetadata) GetDevice(ctx context.Context, name string) (*DeviceInfo, error) {
|
||||||
|
var (
|
||||||
|
dev DeviceInfo
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = m.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket(devicesBucketName)
|
||||||
|
return getObject(bucket, name, &dev)
|
||||||
|
})
|
||||||
|
|
||||||
|
return &dev, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveDevice removes device info from store.
|
||||||
|
func (m *PoolMetadata) RemoveDevice(ctx context.Context, name string) error {
|
||||||
|
return m.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
var (
|
||||||
|
device = &DeviceInfo{}
|
||||||
|
bucket = tx.Bucket(devicesBucketName)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := getObject(bucket, name, device); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bucket.Delete([]byte(name)); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to delete device info for %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := markDeviceID(tx, device.DeviceID, deviceFree); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeviceNames retrieves the list of device names currently stored in database
|
||||||
|
func (m *PoolMetadata) GetDeviceNames(ctx context.Context) ([]string, error) {
|
||||||
|
var (
|
||||||
|
names []string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
err = m.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket(devicesBucketName)
|
||||||
|
return bucket.ForEach(func(k, _ []byte) error {
|
||||||
|
names = append(names, string(k))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes metadata store
|
||||||
|
func (m *PoolMetadata) Close() error {
|
||||||
|
if err := m.db.Close(); err != nil && err != bolt.ErrDatabaseNotOpen {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func putObject(bucket *bolt.Bucket, key string, obj interface{}, overwrite bool) error {
|
||||||
|
keyBytes := []byte(key)
|
||||||
|
|
||||||
|
if !overwrite && bucket.Get(keyBytes) != nil {
|
||||||
|
return errors.Errorf("object with key %q already exists", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to marshal object with key %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bucket.Put(keyBytes, data); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to insert object with key %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getObject(bucket *bolt.Bucket, key string, obj interface{}) error {
|
||||||
|
data := bucket.Get([]byte(key))
|
||||||
|
if data == nil {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj != nil {
|
||||||
|
if err := json.Unmarshal(data, obj); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to unmarshal object with key %q", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
187
snapshots/devmapper/metadata_test.go
Normal file
187
snapshots/devmapper/metadata_test.go
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package devmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testCtx = context.Background()
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPoolMetadata_AddDevice(t *testing.T) {
|
||||||
|
tempDir, store := createStore(t)
|
||||||
|
defer cleanupStore(t, tempDir, store)
|
||||||
|
|
||||||
|
expected := &DeviceInfo{
|
||||||
|
Name: "test2",
|
||||||
|
ParentName: "test1",
|
||||||
|
Size: 1,
|
||||||
|
State: Activated,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := store.AddDevice(testCtx, expected)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
result, err := store.GetDevice(testCtx, "test2")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, expected.Name, result.Name)
|
||||||
|
assert.Equal(t, expected.ParentName, result.ParentName)
|
||||||
|
assert.Equal(t, expected.Size, result.Size)
|
||||||
|
assert.Equal(t, expected.State, result.State)
|
||||||
|
assert.Assert(t, result.DeviceID != 0)
|
||||||
|
assert.Equal(t, expected.DeviceID, result.DeviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolMetadata_AddDeviceRollback(t *testing.T) {
|
||||||
|
tempDir, store := createStore(t)
|
||||||
|
defer cleanupStore(t, tempDir, store)
|
||||||
|
|
||||||
|
err := store.AddDevice(testCtx, &DeviceInfo{Name: ""})
|
||||||
|
assert.Assert(t, err != nil)
|
||||||
|
|
||||||
|
_, err = store.GetDevice(testCtx, "")
|
||||||
|
assert.Equal(t, ErrNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolMetadata_AddDeviceDuplicate(t *testing.T) {
|
||||||
|
tempDir, store := createStore(t)
|
||||||
|
defer cleanupStore(t, tempDir, store)
|
||||||
|
|
||||||
|
err := store.AddDevice(testCtx, &DeviceInfo{Name: "test"})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = store.AddDevice(testCtx, &DeviceInfo{Name: "test"})
|
||||||
|
assert.Equal(t, ErrAlreadyExists, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolMetadata_ReuseDeviceID(t *testing.T) {
|
||||||
|
tempDir, store := createStore(t)
|
||||||
|
defer cleanupStore(t, tempDir, store)
|
||||||
|
|
||||||
|
info1 := &DeviceInfo{Name: "test1"}
|
||||||
|
err := store.AddDevice(testCtx, info1)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
info2 := &DeviceInfo{Name: "test2"}
|
||||||
|
err = store.AddDevice(testCtx, info2)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Assert(t, info1.DeviceID != info2.DeviceID)
|
||||||
|
assert.Assert(t, info1.DeviceID != 0)
|
||||||
|
|
||||||
|
err = store.RemoveDevice(testCtx, info2.Name)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
info3 := &DeviceInfo{Name: "test3"}
|
||||||
|
err = store.AddDevice(testCtx, info3)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, info2.DeviceID, info3.DeviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolMetadata_RemoveDevice(t *testing.T) {
|
||||||
|
tempDir, store := createStore(t)
|
||||||
|
defer cleanupStore(t, tempDir, store)
|
||||||
|
|
||||||
|
err := store.AddDevice(testCtx, &DeviceInfo{Name: "test"})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = store.RemoveDevice(testCtx, "test")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
_, err = store.GetDevice(testCtx, "test")
|
||||||
|
assert.Equal(t, ErrNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolMetadata_UpdateDevice(t *testing.T) {
|
||||||
|
tempDir, store := createStore(t)
|
||||||
|
defer cleanupStore(t, tempDir, store)
|
||||||
|
|
||||||
|
oldInfo := &DeviceInfo{
|
||||||
|
Name: "test1",
|
||||||
|
ParentName: "test2",
|
||||||
|
Size: 3,
|
||||||
|
State: Activated,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := store.AddDevice(testCtx, oldInfo)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = store.UpdateDevice(testCtx, oldInfo.Name, func(info *DeviceInfo) error {
|
||||||
|
info.ParentName = "test5"
|
||||||
|
info.Size = 6
|
||||||
|
info.State = Created
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
newInfo, err := store.GetDevice(testCtx, "test1")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "test1", newInfo.Name)
|
||||||
|
assert.Equal(t, "test5", newInfo.ParentName)
|
||||||
|
assert.Assert(t, newInfo.Size == 6)
|
||||||
|
assert.Equal(t, Created, newInfo.State)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolMetadata_GetDeviceNames(t *testing.T) {
|
||||||
|
tempDir, store := createStore(t)
|
||||||
|
defer cleanupStore(t, tempDir, store)
|
||||||
|
|
||||||
|
err := store.AddDevice(testCtx, &DeviceInfo{Name: "test1"})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = store.AddDevice(testCtx, &DeviceInfo{Name: "test2"})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
names, err := store.GetDeviceNames(testCtx)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(names, 2))
|
||||||
|
|
||||||
|
assert.Equal(t, "test1", names[0])
|
||||||
|
assert.Equal(t, "test2", names[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func createStore(t *testing.T) (tempDir string, store *PoolMetadata) {
|
||||||
|
tempDir, err := ioutil.TempDir("", "pool-metadata-")
|
||||||
|
assert.NilError(t, err, "couldn't create temp directory for metadata tests")
|
||||||
|
|
||||||
|
path := filepath.Join(tempDir, "test.db")
|
||||||
|
metadata, err := NewPoolMetadata(path)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
return tempDir, metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupStore(t *testing.T, tempDir string, store *PoolMetadata) {
|
||||||
|
err := store.Close()
|
||||||
|
assert.NilError(t, err, "failed to close metadata store")
|
||||||
|
|
||||||
|
err = os.RemoveAll(tempDir)
|
||||||
|
assert.NilError(t, err, "failed to cleanup temp directory")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user