package rbd import ( "bytes" "encoding/json" "fmt" "io" "os/exec" "sort" "testing" "time" "github.com/ceph/go-ceph/rados" "github.com/gofrs/uuid/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( testImageSize = uint64(1 << 22) testImageOrder = 22 ) func GetUUID() string { return uuid.Must(uuid.NewV4()).String() } func TestVersion(t *testing.T) { var major, minor, patch = Version() assert.False(t, major < 0 || major > 1000, "invalid major") assert.False(t, minor < 0 || minor > 1000, "invalid minor") assert.False(t, patch < 0 || patch > 1000, "invalid patch") } func radosConnect(t *testing.T) *rados.Conn { conn, err := rados.NewConn() require.NoError(t, err) err = conn.ReadDefaultConfigFile() require.NoError(t, err) waitForRadosConn(t, conn) return conn } func radosConnectConfig(t *testing.T, p string) *rados.Conn { conn, err := rados.NewConn() require.NoError(t, err) err = conn.ReadConfigFile(p) require.NoError(t, err) waitForRadosConn(t, conn) return conn } func waitForRadosConn(t *testing.T, conn *rados.Conn) { var err error timeout := time.After(time.Second * 15) ch := make(chan error) go func(conn *rados.Conn) { ch <- conn.Connect() }(conn) select { case err = <-ch: case <-timeout: err = fmt.Errorf("timed out waiting for connect") } require.NoError(t, err) } func TestImageCreate(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) name := GetUUID() image, err := Create(ioctx, name, testImageSize, testImageOrder) assert.NoError(t, err) err = image.Remove() assert.NoError(t, err) name = GetUUID() image, err = Create(ioctx, name, testImageSize, testImageOrder, FeatureLayering|FeatureStripingV2) assert.NoError(t, err) err = image.Remove() assert.NoError(t, err) name = GetUUID() image, err = Create(ioctx, name, testImageSize, testImageOrder, FeatureLayering|FeatureStripingV2, 4096, 2) assert.NoError(t, err) err = image.Remove() assert.NoError(t, err) // invalid order name = GetUUID() _, err = Create(ioctx, name, testImageSize, -1) assert.Error(t, err) // too many arguments _, err = Create(ioctx, name, testImageSize, testImageOrder, FeatureLayering|FeatureStripingV2, 4096, 2, 123) assert.Error(t, err) ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestImageCreate2(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) assert.NoError(t, err) name := GetUUID() image, err := Create2(ioctx, name, testImageSize, FeatureLayering|FeatureStripingV2, testImageOrder) assert.NoError(t, err) err = image.Remove() assert.NoError(t, err) ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestImageCreate3(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) assert.NoError(t, err) name := GetUUID() image, err := Create3(ioctx, name, testImageSize, FeatureLayering|FeatureStripingV2, testImageOrder, 4096, 2) assert.NoError(t, err) err = image.Remove() assert.NoError(t, err) ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestCreateImageWithOptions(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) assert.NoError(t, err) // nil options, causes a panic if not handled correctly name := GetUUID() options := NewRbdImageOptions() err = CreateImage(nil, name, testImageSize, options) assert.Error(t, err) err = CreateImage(ioctx, "", testImageSize, options) assert.Error(t, err) err = CreateImage(ioctx, name, testImageSize, nil) assert.Error(t, err) // empty/default options name = GetUUID() err = CreateImage(ioctx, name, testImageSize, options) assert.NoError(t, err) err = RemoveImage(ioctx, name) assert.NoError(t, err) // create image with RbdImageOptionOrder err = options.SetUint64(ImageOptionOrder, uint64(testImageOrder)) assert.NoError(t, err) name = GetUUID() err = CreateImage(ioctx, name, testImageSize, options) assert.NoError(t, err) err = RemoveImage(ioctx, name) assert.NoError(t, err) options.Clear() // create image with a different data pool datapool := GetUUID() err = conn.MakePool(datapool) assert.NoError(t, err) err = options.SetString(ImageOptionDataPool, datapool) assert.NoError(t, err) name = GetUUID() err = CreateImage(ioctx, name, testImageSize, options) assert.NoError(t, err) err = RemoveImage(ioctx, name) assert.NoError(t, err) conn.DeletePool(datapool) // cleanup options.Destroy() ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestGetImageNames(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) createdList := []string{} for i := 0; i < 10; i++ { name := GetUUID() err = quickCreate(ioctx, name, testImageSize, testImageOrder) assert.NoError(t, err) createdList = append(createdList, name) } imageNames, err := GetImageNames(ioctx) assert.NoError(t, err) sort.Strings(createdList) sort.Strings(imageNames) assert.Equal(t, createdList, imageNames) for _, name := range createdList { img := GetImage(ioctx, name) err := img.Remove() assert.NoError(t, err) } ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestDeprecatedImageOpen(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) name := GetUUID() image, err := Create(ioctx, name, testImageSize, testImageOrder) assert.NoError(t, err) // an integer is not a valid argument err = image.Open(123) assert.Error(t, err) // open read-write err = image.Open() assert.NoError(t, err) err = image.Close() assert.NoError(t, err) // open read-only err = image.Open(true) assert.NoError(t, err) bytesIn := []byte("input data") _, err = image.Write(bytesIn) // writing should fail in read-only mode assert.Error(t, err) err = image.Remove() // removing should fail while image is opened assert.Equal(t, err, ErrImageIsOpen) err = image.Close() assert.NoError(t, err) err = image.Remove() assert.NoError(t, err) ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestImageResize(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) name := GetUUID() reqSize := uint64(1024 * 1024 * 4) // 4MB err = quickCreate(ioctx, name, reqSize, testImageOrder) assert.NoError(t, err) image, err := OpenImage(ioctx, name, NoSnapshot) assert.NoError(t, err) size, err := image.GetSize() assert.NoError(t, err) assert.Equal(t, size, reqSize) err = image.Resize(reqSize * 2) assert.NoError(t, err) size, err = image.GetSize() assert.NoError(t, err) assert.Equal(t, size, reqSize*2) err = image.Close() assert.NoError(t, err) err = image.Resize(reqSize * 2) assert.Equal(t, err, ErrImageNotOpen) err = image.Remove() assert.NoError(t, err) ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestImageProperties(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) require.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) name := GetUUID() reqSize := uint64(1024 * 1024 * 4) // 4MB _, err = Create3(ioctx, name, reqSize, FeatureLayering|FeatureStripingV2, testImageOrder, 4096, 2) require.NoError(t, err) img, err := OpenImage(ioctx, name, NoSnapshot) require.NoError(t, err) format, err := img.IsOldFormat() assert.NoError(t, err) assert.Equal(t, format, false) size, err := img.GetSize() assert.NoError(t, err) assert.Equal(t, size, reqSize) features, err := img.GetFeatures() assert.NoError(t, err) // compare features with the two requested ones assert.Equal(t, features&(FeatureLayering|FeatureStripingV2), FeatureLayering|FeatureStripingV2) stripeUnit, err := img.GetStripeUnit() assert.NoError(t, err) assert.Equal(t, stripeUnit, uint64(4096)) stripeCount, err := img.GetStripeCount() assert.NoError(t, err) assert.Equal(t, stripeCount, uint64(2)) _, err = img.GetOverlap() assert.NoError(t, err) err = img.Close() assert.NoError(t, err) err = img.Remove() assert.NoError(t, err) ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestImageRename(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) name := GetUUID() err = quickCreate(ioctx, name, testImageSize, testImageOrder) assert.NoError(t, err) img := GetImage(ioctx, name) err = img.Rename(name) assert.Error(t, err) err = img.Rename(GetUUID()) assert.NoError(t, err) err = img.Remove() assert.NoError(t, err) ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestImageSeek(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) name := GetUUID() err = quickCreate(ioctx, name, testImageSize, testImageOrder) assert.NoError(t, err) img, err := OpenImage(ioctx, name, NoSnapshot) assert.NoError(t, err) _, err = img.Seek(0, SeekSet) assert.NoError(t, err) bytesIn := []byte("input data") numIn, err := img.Write(bytesIn) assert.NoError(t, err) assert.Equal(t, numIn, len(bytesIn)) pos, err := img.Seek(0, SeekCur) assert.NoError(t, err) assert.Equal(t, pos, int64(numIn)) pos, err = img.Seek(0, SeekSet) assert.NoError(t, err) assert.Equal(t, pos, int64(0)) bytesOut := make([]byte, len(bytesIn)) numOut, err := img.Read(bytesOut) assert.NoError(t, err) assert.Equal(t, numOut, len(bytesOut)) assert.Equal(t, bytesIn, bytesOut) pos, err = img.Seek(0, SeekCur) assert.NoError(t, err) assert.Equal(t, pos, int64(numOut)) pos, err = img.Seek(0, SeekSet) assert.NoError(t, err) assert.Equal(t, pos, int64(0)) pos, err = img.Seek(0, SeekEnd) assert.NoError(t, err) assert.Equal(t, pos, int64(testImageSize)) _, err = img.Seek(0, -1) assert.Error(t, err) err = img.Close() assert.NoError(t, err) _, err = img.Seek(0, SeekEnd) assert.Equal(t, err, ErrImageNotOpen) err = img.Remove() assert.NoError(t, err) ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestImageDiscard(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) name := GetUUID() err = quickCreate(ioctx, name, testImageSize, testImageOrder) assert.NoError(t, err) img, err := OpenImage(ioctx, name, NoSnapshot) assert.NoError(t, err) n, err := img.Discard(0, 1<<16) assert.NoError(t, err) assert.Equal(t, n, 1<<16) err = img.Close() assert.NoError(t, err) img, err = OpenImageReadOnly(ioctx, name, NoSnapshot) assert.NoError(t, err) // when read-only, discard should fail _, err = img.Discard(0, 1<<16) assert.Error(t, err) err = img.Close() assert.NoError(t, err) _, err = img.Discard(0, 1<<16) assert.Equal(t, err, ErrImageNotOpen) err = img.Remove() assert.NoError(t, err) ioctx.Destroy() conn.DeletePool(poolname) conn.Shutdown() } func TestWriteSame(t *testing.T) { conn := radosConnect(t) poolname := GetUUID() err := conn.MakePool(poolname) assert.NoError(t, err) ioctx, err := conn.OpenIOContext(poolname) require.NoError(t, err) name := GetUUID() options := NewRbdImageOptions() defer options.Destroy() err = options.SetUint64(ImageOptionOrder, uint64(testImageOrder)) assert.NoError(t, err) err = CreateImage(ioctx, name, testImageSize, options) require.NoError(t, err) // WriteSame() by default calls discard when writing only zeros. This // can be unexpected when trying to allocate a whole RBD image. To // disable this behaviour, and actually write zeros, the option // `rbd_discard_on_zeroed_write_same` needs to be disabled. conn.SetConfigOption("rbd_discard_on_zeroed_write_same", "false") img, err := OpenImage(ioctx, name, NoSnapshot) assert.NoError(t, err) dataOut := []byte("this is a string of 28 bytes") t.Run("writeAndRead", func(t *testing.T) { // write some bytes at the start of the image numOut, err := img.WriteSame(0, uint64(4*len(dataOut)), dataOut, rados.OpFlagNone) assert.Equal(t, int64(4*len(dataOut)), numOut) assert.NoError(t, err) // the same string should be read from byte 0 dataIn := make([]byte, len(dataOut)) numIn, err := img.ReadAt(dataIn, 0) assert.Equal(t, len(dataOut), numIn) assert.Equal(t, dataOut, dataIn) assert.NoError(t, err) // the same string should be read from byte 28 dataIn = make([]byte, len(dataOut)) numIn, err = img.ReadAt(dataIn, 28) assert.Equal(t, len(dataOut), numIn) assert.Equal(t, dataOut, dataIn) assert.NoError(t, err) }) t.Run("writePartialData", func(t *testing.T) { // writing a non-multiple of dataOut len will fail _, err = img.WriteSame(0, 64, dataOut, rados.OpFlagNone) assert.Error(t, err) }) t.Run("writeNoData", func(t *testing.T) { // writing empty data should succeed numIn, err := img.WriteSame(0, 64, []byte(""), rados.OpFlagNone) assert.NoError(t, err) assert.Equal(t, int64(0), numIn) }) t.Run("zerofill", func(t *testing.T) { // implement allocating an image by writing zeros to it sc, err := img.GetStripeCount() assert.NoError(t, err) // fetch the actual size of the image as available in the pool, can be // margially different from the requested image size st, err := img.Stat() assert.NoError(t, err) size := st.Size // zeroBlock is the stripe-period: size of the object-size multiplied // by the stripe-count order := uint(st.Order) // signed shifting requires newer golang?! zeroBlock := make([]byte, sc*(1<