Repository: ceph/go-ceph
Pull request #371 (2020-09-24 20:00:43)

cephfs admin:  subvolume snapshot support
Add support for subvolume and subvolumegroup snapshots. Add support for SubVolumeSnapshotInfo.

This PR builds on top of the work in PR #366 please review and merge that first.


## Checklist
- [x] Added tests for features and functional changes
- [x] Public functions and types are documented
- [x] Standard formatting is applied to Go code



Patch:
diff --git a/cephfs/admin/subvolume.go b/cephfs/admin/subvolume.go
index d17ce32..ac3cb90 100644
--- a/cephfs/admin/subvolume.go
+++ b/cephfs/admin/subvolume.go
@@ -211,3 +211,56 @@ func (fsa *FSAdmin) SubVolumeInfo(volume, group, name string) (*SubVolumeInfo, e
 	}
 	return parseSubVolumeInfo(fsa.marshalMgrCommand(m))
 }
+
+// CreateSubVolumeSnapshot creates a new snapshot from the source subvolume.
+//
+// Similar To:
+//  ceph fs subvolume snapshot create <volume> --group-name=<group> <source> <name>
+func (fsa *FSAdmin) CreateSubVolumeSnapshot(volume, group, source, name string) error {
+	m := map[string]string{
+		"prefix":    "fs subvolume snapshot create",
+		"vol_name":  volume,
+		"sub_name":  source,
+		"snap_name": name,
+		"format":    "json",
+	}
+	if group != NoGroup {
+		m["group_name"] = group
+	}
+	return checkEmptyResponseExpected(fsa.marshalMgrCommand(m))
+}
+
+// RemoveSubVolumeSnapshot removes the specified snapshot from the subvolume.
+//
+// Similar To:
+//  ceph fs subvolume snapshot rm <volume> --group-name=<group> <subvolume> <name>
+func (fsa *FSAdmin) RemoveSubVolumeSnapshot(volume, group, subvolume, name string) error {
+	m := map[string]string{
+		"prefix":    "fs subvolume snapshot rm",
+		"vol_name":  volume,
+		"sub_name":  subvolume,
+		"snap_name": name,
+		"format":    "json",
+	}
+	if group != NoGroup {
+		m["group_name"] = group
+	}
+	return checkEmptyResponseExpected(fsa.marshalMgrCommand(m))
+}
+
+// ListSubVolumeSnapshots returns a listing of snapshots for a given subvolume.
+//
+// Similar To:
+//  ceph fs subvolume snapshot ls <volume> --group-name=<group> <name>
+func (fsa *FSAdmin) ListSubVolumeSnapshots(volume, group, name string) ([]string, error) {
+	m := map[string]string{
+		"prefix":   "fs subvolume snapshot ls",
+		"vol_name": volume,
+		"sub_name": name,
+		"format":   "json",
+	}
+	if group != NoGroup {
+		m["group_name"] = group
+	}
+	return parseListNames(fsa.marshalMgrCommand(m))
+}
diff --git a/cephfs/admin/subvolume_octopus.go b/cephfs/admin/subvolume_octopus.go
new file mode 100644
index 0000000..615712b
--- /dev/null
+++ b/cephfs/admin/subvolume_octopus.go
@@ -0,0 +1,38 @@
+// +build octopus
+
+package admin
+
+// SubVolumeSnapshotInfo reports various informational values about a subvolume.
+type SubVolumeSnapshotInfo struct {
+	CreatedAt        TimeStamp `json:"created_at"`
+	DataPool         string    `json:"data_pool"`
+	HasPendingClones string    `json:"has_pending_clones"`
+	Protected        string    `json:"protected"`
+	Size             ByteCount `json:"size"`
+}
+
+func parseSubVolumeSnapshotInfo(r []byte, s string, err error) (*SubVolumeSnapshotInfo, error) {
+	var info SubVolumeSnapshotInfo
+	if err := unmarshalResponseJSON(r, s, err, &info); err != nil {
+		return nil, err
+	}
+	return &info, nil
+}
+
+// SubVolumeSnapshotInfo returns information about the specified subvolume snapshot.
+//
+// Similar To:
+//  ceph fs subvolume snapshot info <volume> --group-name=<group> <subvolume> <name>
+func (fsa *FSAdmin) SubVolumeSnapshotInfo(volume, group, subvolume, name string) (*SubVolumeSnapshotInfo, error) {
+	m := map[string]string{
+		"prefix":    "fs subvolume snapshot info",
+		"vol_name":  volume,
+		"sub_name":  subvolume,
+		"snap_name": name,
+		"format":    "json",
+	}
+	if group != NoGroup {
+		m["group_name"] = group
+	}
+	return parseSubVolumeSnapshotInfo(fsa.marshalMgrCommand(m))
+}
diff --git a/cephfs/admin/subvolumegroup.go b/cephfs/admin/subvolumegroup.go
index 1a672db..03b77b2 100644
--- a/cephfs/admin/subvolumegroup.go
+++ b/cephfs/admin/subvolumegroup.go
@@ -92,3 +92,47 @@ func (fsa *FSAdmin) SubVolumeGroupPath(volume, name string) (string, error) {
 	}
 	return extractPathResponse(fsa.marshalMgrCommand(m))
 }
+
+// CreateSubVolumeGroupSnapshot creates a new snapshot from the source subvolume group.
+//
+// Similar To:
+//  ceph fs subvolumegroup snapshot create <volume> <group> <name>
+func (fsa *FSAdmin) CreateSubVolumeGroupSnapshot(volume, group, name string) error {
+	m := map[string]string{
+		"prefix":     "fs subvolumegroup snapshot create",
+		"vol_name":   volume,
+		"group_name": group,
+		"snap_name":  name,
+		"format":     "json",
+	}
+	return checkEmptyResponseExpected(fsa.marshalMgrCommand(m))
+}
+
+// RemoveSubVolumeGroupSnapshot removes the specified snapshot from the subvolume group.
+//
+// Similar To:
+//  ceph fs subvolumegroup snapshot rm <volume> <group> <name>
+func (fsa *FSAdmin) RemoveSubVolumeGroupSnapshot(volume, group, name string) error {
+	m := map[string]string{
+		"prefix":     "fs subvolumegroup snapshot rm",
+		"vol_name":   volume,
+		"group_name": group,
+		"snap_name":  name,
+		"format":     "json",
+	}
+	return checkEmptyResponseExpected(fsa.marshalMgrCommand(m))
+}
+
+// ListSubVolumeGroupSnapshots returns a listing of snapshots for a given subvolume group.
+//
+// Similar To:
+//  ceph fs subvolumegroup snapshot ls <volume> <group>
+func (fsa *FSAdmin) ListSubVolumeGroupSnapshots(volume, group string) ([]string, error) {
+	m := map[string]string{
+		"prefix":     "fs subvolumegroup snapshot ls",
+		"vol_name":   volume,
+		"group_name": group,
+		"format":     "json",
+	}
+	return parseListNames(fsa.marshalMgrCommand(m))
+}


Test patch:
diff --git a/cephfs/admin/subvolume_octopus_test.go b/cephfs/admin/subvolume_octopus_test.go
new file mode 100644
index 0000000..163cbbb
--- /dev/null
+++ b/cephfs/admin/subvolume_octopus_test.go
@@ -0,0 +1,92 @@
+// +build octopus
+
+package admin
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var sampleSubVolumeSnapshoInfo1 = []byte(`
+{
+    "created_at": "2020-09-11 17:40:12.035792",
+    "data_pool": "cephfs_data",
+    "has_pending_clones": "no",
+    "protected": "yes",
+    "size": 0
+}
+`)
+
+func TestParseSubVolumeSnapshotInfo(t *testing.T) {
+	t.Run("error", func(t *testing.T) {
+		_, err := parseSubVolumeSnapshotInfo(nil, "", errors.New("flub"))
+		assert.Error(t, err)
+		assert.Equal(t, "flub", err.Error())
+	})
+	t.Run("statusSet", func(t *testing.T) {
+		_, err := parseSubVolumeSnapshotInfo(nil, "unexpected!", nil)
+		assert.Error(t, err)
+	})
+	t.Run("badJSON", func(t *testing.T) {
+		_, err := parseSubVolumeSnapshotInfo([]byte("_XxXxX"), "", nil)
+		assert.Error(t, err)
+	})
+	t.Run("ok", func(t *testing.T) {
+		info, err := parseSubVolumeSnapshotInfo(sampleSubVolumeSnapshoInfo1, "", nil)
+		assert.NoError(t, err)
+		if assert.NotNil(t, info) {
+			assert.Equal(t, "cephfs_data", info.DataPool)
+			assert.EqualValues(t, 0, info.Size)
+			assert.Equal(t, 2020, info.CreatedAt.Year())
+			assert.Equal(t, "yes", info.Protected)
+			assert.Equal(t, "no", info.HasPendingClones)
+		}
+	})
+}
+
+func TestSubVolumeSnapshotInfo(t *testing.T) {
+	fsa := getFSAdmin(t)
+	volume := "cephfs"
+	group := "20000leagues"
+	subname := "poulp"
+	snapname1 := "t1"
+	snapname2 := "nope"
+
+	err := fsa.CreateSubVolumeGroup(volume, group, nil)
+	assert.NoError(t, err)
+	defer func() {
+		err := fsa.RemoveSubVolumeGroup(volume, group)
+		assert.NoError(t, err)
+	}()
+
+	svopts := &SubVolumeOptions{
+		Mode: 0750,
+		Size: 20 * gibiByte,
+	}
+	err = fsa.CreateSubVolume(volume, group, subname, svopts)
+	assert.NoError(t, err)
+	defer func() {
+		err := fsa.RemoveSubVolume(volume, group, subname)
+		assert.NoError(t, err)
+	}()
+
+	err = fsa.CreateSubVolumeSnapshot(volume, group, subname, snapname1)
+	assert.NoError(t, err)
+	defer func() {
+		err := fsa.RemoveSubVolumeSnapshot(volume, group, subname, snapname1)
+		assert.NoError(t, err)
+	}()
+
+	sinfo, err := fsa.SubVolumeSnapshotInfo(volume, group, subname, snapname1)
+	assert.NoError(t, err)
+	assert.NotNil(t, sinfo)
+	assert.EqualValues(t, 0, sinfo.Size)
+	assert.Equal(t, "cephfs_data", sinfo.DataPool)
+	assert.GreaterOrEqual(t, 2020, sinfo.CreatedAt.Year())
+
+	sinfo, err = fsa.SubVolumeSnapshotInfo(volume, group, subname, snapname2)
+	assert.Error(t, err)
+	assert.Nil(t, sinfo)
+}
diff --git a/cephfs/admin/subvolume_test.go b/cephfs/admin/subvolume_test.go
index f78f1da..5a54ccf 100644
--- a/cephfs/admin/subvolume_test.go
+++ b/cephfs/admin/subvolume_test.go
@@ -347,3 +347,72 @@ func TestSubVolumeInfo(t *testing.T) {
 	assert.Equal(t, 040750, vinfo.Mode)
 	assert.GreaterOrEqual(t, 2020, vinfo.Ctime.Year())
 }
+
+func TestSubVolumeSnapshots(t *testing.T) {
+	fsa := getFSAdmin(t)
+	volume := "cephfs"
+	group := "20000leagues"
+	subname := "nautilus"
+	snapname1 := "ne1"
+	snapname2 := "mo2"
+
+	err := fsa.CreateSubVolumeGroup(volume, group, nil)
+	assert.NoError(t, err)
+	defer func() {
+		err := fsa.RemoveSubVolumeGroup(volume, group)
+		assert.NoError(t, err)
+	}()
+
+	svopts := &SubVolumeOptions{
+		Mode: 0750,
+		Size: 20 * gibiByte,
+	}
+	err = fsa.CreateSubVolume(volume, group, subname, svopts)
+	assert.NoError(t, err)
+	defer func() {
+		err := fsa.RemoveSubVolume(volume, group, subname)
+		assert.NoError(t, err)
+	}()
+
+	t.Run("createAndRemove", func(t *testing.T) {
+		err = fsa.CreateSubVolumeSnapshot(volume, group, subname, snapname1)
+		assert.NoError(t, err)
+		err := fsa.RemoveSubVolumeSnapshot(volume, group, subname, snapname1)
+		assert.NoError(t, err)
+	})
+
+	t.Run("listOne", func(t *testing.T) {
+		err = fsa.CreateSubVolumeSnapshot(volume, group, subname, snapname1)
+		assert.NoError(t, err)
+		defer func() {
+			err := fsa.RemoveSubVolumeSnapshot(volume, group, subname, snapname1)
+			assert.NoError(t, err)
+		}()
+
+		snaps, err := fsa.ListSubVolumeSnapshots(volume, group, subname)
+		assert.NoError(t, err)
+		assert.Len(t, snaps, 1)
+		assert.Contains(t, snaps, snapname1)
+	})
+
+	t.Run("listTwo", func(t *testing.T) {
+		err = fsa.CreateSubVolumeSnapshot(volume, group, subname, snapname1)
+		assert.NoError(t, err)
+		defer func() {
+			err := fsa.RemoveSubVolumeSnapshot(volume, group, subname, snapname1)
+			assert.NoError(t, err)
+		}()
+		err = fsa.CreateSubVolumeSnapshot(volume, group, subname, snapname2)
+		assert.NoError(t, err)
+		defer func() {
+			err := fsa.RemoveSubVolumeSnapshot(volume, group, subname, snapname2)
+			assert.NoError(t, err)
+		}()
+
+		snaps, err := fsa.ListSubVolumeSnapshots(volume, group, subname)
+		assert.NoError(t, err)
+		assert.Len(t, snaps, 2)
+		assert.Contains(t, snaps, snapname1)
+		assert.Contains(t, snaps, snapname2)
+	})
+}
diff --git a/cephfs/admin/subvolumegroup_test.go b/cephfs/admin/subvolumegroup_test.go
index ea2ea54..b57e99c 100644
--- a/cephfs/admin/subvolumegroup_test.go
+++ b/cephfs/admin/subvolumegroup_test.go
@@ -110,3 +110,77 @@ func TestSubVolumeGroupPath(t *testing.T) {
 	assert.Error(t, err)
 	assert.Equal(t, "", path)
 }
+
+func TestSubVolumeGroupSnapshots(t *testing.T) {
+	fsa := getFSAdmin(t)
+	volume := "cephfs"
+	group := "bathyscaphe"
+	subname := "trieste"
+	snapname1 := "ns1"
+	snapname2 := "ns2"
+
+	err := fsa.CreateSubVolumeGroup(volume, group, nil)
+	assert.NoError(t, err)
+	defer func() {
+		err := fsa.RemoveSubVolumeGroup(volume, group)
+		assert.NoError(t, err)
+	}()
+
+	svopts := &SubVolumeOptions{
+		Mode: 0750,
+		Size: 20 * gibiByte,
+	}
+	err = fsa.CreateSubVolume(volume, group, subname, svopts)
+	assert.NoError(t, err)
+	defer func() {
+		err := fsa.RemoveSubVolume(volume, group, subname)
+		assert.NoError(t, err)
+	}()
+
+	t.Run("createAndRemove", func(t *testing.T) {
+		err = fsa.CreateSubVolumeGroupSnapshot(volume, group, snapname1)
+		assert.NoError(t, err)
+		err := fsa.RemoveSubVolumeGroupSnapshot(volume, group, snapname1)
+		assert.NoError(t, err)
+	})
+
+	t.Run("listOne", func(t *testing.T) {
+		err = fsa.CreateSubVolumeGroupSnapshot(volume, group, snapname1)
+		assert.NoError(t, err)
+		defer func() {
+			err := fsa.RemoveSubVolumeGroupSnapshot(volume, group, snapname1)
+			assert.NoError(t, err)
+		}()
+
+		snaps, err := fsa.ListSubVolumeGroupSnapshots(volume, group)
+		assert.NoError(t, err)
+		assert.Len(t, snaps, 1)
+		assert.Contains(t, snaps, snapname1)
+	})
+
+	t.Run("listTwo", func(t *testing.T) {
+		err = fsa.CreateSubVolumeGroupSnapshot(volume, group, snapname1)
+		assert.NoError(t, err)
+		defer func() {
+			err := fsa.RemoveSubVolumeGroupSnapshot(volume, group, snapname1)
+			assert.NoError(t, err)
+		}()
+		err = fsa.CreateSubVolumeGroupSnapshot(volume, group, snapname2)
+		assert.NoError(t, err)
+		defer func() {
+			err := fsa.RemoveSubVolumeGroupSnapshot(volume, group, snapname2)
+			assert.NoError(t, err)
+		}()
+
+		snaps, err := fsa.ListSubVolumeGroupSnapshots(volume, group)
+		assert.NoError(t, err)
+		assert.Len(t, snaps, 2)
+		assert.Contains(t, snaps, snapname1)
+		assert.Contains(t, snaps, snapname2)
+
+		// subvolumegroup snaps are reflected in subvolumes (with mangled names)
+		snaps, err = fsa.ListSubVolumeSnapshots(volume, group, subname)
+		assert.NoError(t, err)
+		assert.Len(t, snaps, 2)
+	})
+}