cephfs admin: add basic functionality for clone support

I think I'm a clone now

Signed-off-by: John Mulligan <jmulligan@redhat.com>
This commit is contained in:
John Mulligan 2020-09-17 17:10:47 -04:00 committed by John Mulligan
parent beb3351f24
commit 07722a0111
3 changed files with 285 additions and 0 deletions

111
cephfs/admin/clone.go Normal file
View File

@ -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 <volume> <subvolume> <snapshot> <name>
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 <volume> --group_name=<group> <clone>
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 <volume> --group_name=<group> <clone>
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))
}

138
cephfs/admin/clone_test.go Normal file
View File

@ -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)
}
}

View File

@ -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 <volume> --group-name=<group> <subvolume> <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 <volume> --group-name=<group> <subvolume> <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))
}