mirror of https://github.com/ceph/go-ceph
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:
parent
beb3351f24
commit
07722a0111
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue