diff --git a/cephfs/admin/clone.go b/cephfs/admin/clone.go new file mode 100644 index 0000000..5e01399 --- /dev/null +++ b/cephfs/admin/clone.go @@ -0,0 +1,111 @@ +// +build !luminous,!mimic + +package admin + +// CloneOptions are used to specify optional values to be used when creating a +// new subvolume clone. +type CloneOptions struct { + TargetGroup string + PoolLayout string +} + +// CloneSubVolumeSnapshot Clones the specified snapshot from the subvolume. +// +// Similar To: +// ceph fs subvolume snapshot clone +func (fsa *FSAdmin) CloneSubVolumeSnapshot(volume, group, subvolume, snapshot, name string, o *CloneOptions) error { + m := map[string]string{ + "prefix": "fs subvolume snapshot clone", + "vol_name": volume, + "sub_name": subvolume, + "snap_name": snapshot, + "target_sub_name": name, + "format": "json", + } + if group != NoGroup { + m["group_name"] = group + } + if o != nil && o.TargetGroup != NoGroup { + m["target_group_name"] = group + } + if o != nil && o.PoolLayout != "" { + m["pool_layout"] = o.PoolLayout + } + return checkEmptyResponseExpected(fsa.marshalMgrCommand(m)) +} + +// CloneState is used to define constant values used to determine the state of +// a clone. +type CloneState string + +const ( + // ClonePending is the state of a pending clone. + ClonePending = CloneState("pending") + // CloneInProgress is the state of a clone in progress. + CloneInProgress = CloneState("in-progress") + // CloneComplete is the state of a complete clone. + CloneComplete = CloneState("complete") + // CloneFailed is the state of a failed clone. + CloneFailed = CloneState("failed") +) + +// CloneSource contains values indicating the source of a clone. +type CloneSource struct { + Volume string `json:"volume"` + Group string `json:"group"` + SubVolume string `json:"subvolume"` + Snapshot string `json:"snapshot"` +} + +// CloneStatus reports on the status of a subvolume clone. +type CloneStatus struct { + State CloneState `json:"state"` + Source CloneSource `json:"source"` +} + +type cloneStatusWrapper struct { + Status CloneStatus `json:"status"` +} + +func parseCloneStatus(r []byte, s string, err error) (*CloneStatus, error) { + var status cloneStatusWrapper + if err := unmarshalResponseJSON(r, s, err, &status); err != nil { + return nil, err + } + return &status.Status, nil +} + +// CloneStatus returns data reporting the status of a subvolume clone. +// +// Similar To: +// ceph fs clone status --group_name= +func (fsa *FSAdmin) CloneStatus(volume, group, clone string) (*CloneStatus, error) { + m := map[string]string{ + "prefix": "fs clone status", + "vol_name": volume, + "clone_name": clone, + "format": "json", + } + if group != NoGroup { + m["group_name"] = group + } + return parseCloneStatus(fsa.marshalMgrCommand(m)) +} + +// CancelClone stops the background processes that populate a clone. +// CancelClone does not delete the clone. +// +// Similar To: +// ceph fs clone cancel --group_name= +func (fsa *FSAdmin) CancelClone(volume, group, clone string) error { + m := map[string]string{ + "prefix": "fs clone cancel", + "vol_name": volume, + "clone_name": clone, + "format": "json", + } + if group != NoGroup { + m["group_name"] = group + } + return checkEmptyResponseExpected(fsa.marshalMgrCommand(m)) +} diff --git a/cephfs/admin/clone_test.go b/cephfs/admin/clone_test.go new file mode 100644 index 0000000..742cf73 --- /dev/null +++ b/cephfs/admin/clone_test.go @@ -0,0 +1,138 @@ +// +build !luminous,!mimic + +package admin + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var sampleCloneStatusPending = []byte(`{ + "status": { + "state": "pending", + "source": { + "volume": "cephfs", + "subvolume": "jurrasic", + "snapshot": "dinodna", + "group": "park" + } + } +}`) + +var sampleCloneStatusInProg = []byte(`{ + "status": { + "state": "in-progress", + "source": { + "volume": "cephfs", + "subvolume": "subvol1", + "snapshot": "snap1" + } + } +}`) + +func TestParseCloneStatus(t *testing.T) { + t.Run("error", func(t *testing.T) { + _, err := parseCloneStatus(nil, "", errors.New("flub")) + assert.Error(t, err) + assert.Equal(t, "flub", err.Error()) + }) + t.Run("statusSet", func(t *testing.T) { + _, err := parseCloneStatus(nil, "unexpected!", nil) + assert.Error(t, err) + }) + t.Run("badJSON", func(t *testing.T) { + _, err := parseCloneStatus([]byte("_XxXxX"), "", nil) + assert.Error(t, err) + }) + t.Run("okPending", func(t *testing.T) { + status, err := parseCloneStatus(sampleCloneStatusPending, "", nil) + assert.NoError(t, err) + if assert.NotNil(t, status) { + assert.EqualValues(t, ClonePending, status.State) + assert.EqualValues(t, "cephfs", status.Source.Volume) + assert.EqualValues(t, "jurrasic", status.Source.SubVolume) + assert.EqualValues(t, "dinodna", status.Source.Snapshot) + assert.EqualValues(t, "park", status.Source.Group) + } + }) + t.Run("okInProg", func(t *testing.T) { + status, err := parseCloneStatus(sampleCloneStatusInProg, "", nil) + assert.NoError(t, err) + if assert.NotNil(t, status) { + assert.EqualValues(t, CloneInProgress, status.State) + assert.EqualValues(t, "cephfs", status.Source.Volume) + assert.EqualValues(t, "subvol1", status.Source.SubVolume) + assert.EqualValues(t, "snap1", status.Source.Snapshot) + assert.EqualValues(t, "", status.Source.Group) + } + }) +} + +func TestCloneSubVolumeSnapshot(t *testing.T) { + fsa := getFSAdmin(t) + volume := "cephfs" + group := "Park" + subname := "Jurrasic" + snapname := "dinodna0" + clonename := "babydino" + + 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, snapname) + assert.NoError(t, err) + defer func() { + err := fsa.RemoveSubVolumeSnapshot(volume, group, subname, snapname) + assert.NoError(t, err) + }() + + err = fsa.ProtectSubVolumeSnapshot(volume, group, subname, snapname) + assert.NoError(t, err) + defer func() { + err := fsa.UnprotectSubVolumeSnapshot(volume, group, subname, snapname) + assert.NoError(t, err) + }() + + err = fsa.CloneSubVolumeSnapshot( + volume, group, subname, snapname, clonename, + &CloneOptions{TargetGroup: group}) + assert.NoError(t, err) + defer func() { + err := fsa.RemoveSubVolume(volume, group, clonename) + assert.NoError(t, err) + }() + + for done := false; !done; { + status, err := fsa.CloneStatus(volume, group, clonename) + assert.NoError(t, err) + assert.NotNil(t, status) + switch status.State { + case ClonePending, CloneInProgress: + case CloneComplete: + done = true + case CloneFailed: + t.Fatal("clone failed") + default: + t.Fatalf("invalid status.State: %q", status.State) + } + time.Sleep(5 * time.Millisecond) + } +} diff --git a/cephfs/admin/subvolume.go b/cephfs/admin/subvolume.go index ac3cb90..22d3295 100644 --- a/cephfs/admin/subvolume.go +++ b/cephfs/admin/subvolume.go @@ -264,3 +264,39 @@ func (fsa *FSAdmin) ListSubVolumeSnapshots(volume, group, name string) ([]string } return parseListNames(fsa.marshalMgrCommand(m)) } + +// ProtectSubVolumeSnapshot protects the specified snapshot. +// +// Similar To: +// ceph fs subvolume snapshot protect --group-name= +func (fsa *FSAdmin) ProtectSubVolumeSnapshot(volume, group, subvolume, name string) error { + m := map[string]string{ + "prefix": "fs subvolume snapshot protect", + "vol_name": volume, + "sub_name": subvolume, + "snap_name": name, + "format": "json", + } + if group != NoGroup { + m["group_name"] = group + } + return checkEmptyResponseExpected(fsa.marshalMgrCommand(m)) +} + +// UnprotectSubVolumeSnapshot removes protection from the specified snapshot. +// +// Similar To: +// ceph fs subvolume snapshot unprotect --group-name= +func (fsa *FSAdmin) UnprotectSubVolumeSnapshot(volume, group, subvolume, name string) error { + m := map[string]string{ + "prefix": "fs subvolume snapshot unprotect", + "vol_name": volume, + "sub_name": subvolume, + "snap_name": name, + "format": "json", + } + if group != NoGroup { + m["group_name"] = group + } + return checkEmptyResponseExpected(fsa.marshalMgrCommand(m)) +}