mirror of
https://github.com/ceph/ceph-csi.git
synced 2025-06-14 10:53:34 +00:00
cleanup: migration of volrep to csi-addons
This commit moves the volrep logic from internal/rbd to internal/csi-addons/rbd. Signed-off-by: riya-singhal31 <rsinghal@redhat.com>
This commit is contained in:
committed by
mergify[bot]
parent
fb930243a8
commit
304194a0c0
@ -943,7 +943,7 @@ func (cs *ControllerServer) DeleteVolume(
|
||||
func cleanupRBDImage(ctx context.Context,
|
||||
rbdVol *rbdVolume, cr *util.Credentials,
|
||||
) (*csi.DeleteVolumeResponse, error) {
|
||||
mirroringInfo, err := rbdVol.getImageMirroringInfo()
|
||||
mirroringInfo, err := rbdVol.GetImageMirroringInfo()
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
@ -962,7 +962,7 @@ func cleanupRBDImage(ctx context.Context,
|
||||
// the image on all the remote (secondary) clusters will get
|
||||
// auto-deleted. This helps in garbage collecting the OMAP, PVC and PV
|
||||
// objects after failback operation.
|
||||
localStatus, rErr := rbdVol.getLocalState()
|
||||
localStatus, rErr := rbdVol.GetLocalState()
|
||||
if rErr != nil {
|
||||
return nil, status.Error(codes.Internal, rErr.Error())
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ type Driver struct {
|
||||
ids *rbd.IdentityServer
|
||||
ns *rbd.NodeServer
|
||||
cs *rbd.ControllerServer
|
||||
rs *rbd.ReplicationServer
|
||||
rs *casrbd.ReplicationServer
|
||||
|
||||
// cas is the CSIAddonsServer where CSI-Addons services are handled
|
||||
cas *csiaddons.CSIAddonsServer
|
||||
@ -66,8 +66,8 @@ func NewControllerServer(d *csicommon.CSIDriver) *rbd.ControllerServer {
|
||||
}
|
||||
}
|
||||
|
||||
func NewReplicationServer(c *rbd.ControllerServer) *rbd.ReplicationServer {
|
||||
return &rbd.ReplicationServer{ControllerServer: c}
|
||||
func NewReplicationServer(c *rbd.ControllerServer) *casrbd.ReplicationServer {
|
||||
return &casrbd.ReplicationServer{ControllerServer: c}
|
||||
}
|
||||
|
||||
// NewNodeServer initialize a node server for rbd CSI driver.
|
||||
|
@ -25,8 +25,8 @@ import (
|
||||
librbd "github.com/ceph/go-ceph/rbd"
|
||||
)
|
||||
|
||||
// enableImageMirroring enables mirroring on an image.
|
||||
func (ri *rbdImage) enableImageMirroring(mode librbd.ImageMirrorMode) error {
|
||||
// EnableImageMirroring enables mirroring on an image.
|
||||
func (ri *rbdImage) EnableImageMirroring(mode librbd.ImageMirrorMode) error {
|
||||
image, err := ri.open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open image %q with error: %w", ri, err)
|
||||
@ -41,8 +41,8 @@ func (ri *rbdImage) enableImageMirroring(mode librbd.ImageMirrorMode) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// disableImageMirroring disables mirroring on an image.
|
||||
func (ri *rbdImage) disableImageMirroring(force bool) error {
|
||||
// DisableImageMirroring disables mirroring on an image.
|
||||
func (ri *rbdImage) DisableImageMirroring(force bool) error {
|
||||
image, err := ri.open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open image %q with error: %w", ri, err)
|
||||
@ -57,8 +57,8 @@ func (ri *rbdImage) disableImageMirroring(force bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getImageMirroringInfo gets mirroring information of an image.
|
||||
func (ri *rbdImage) getImageMirroringInfo() (*librbd.MirrorImageInfo, error) {
|
||||
// GetImageMirroringInfo gets mirroring information of an image.
|
||||
func (ri *rbdImage) GetImageMirroringInfo() (*librbd.MirrorImageInfo, error) {
|
||||
image, err := ri.open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open image %q with error: %w", ri, err)
|
||||
@ -73,8 +73,8 @@ func (ri *rbdImage) getImageMirroringInfo() (*librbd.MirrorImageInfo, error) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// promoteImage promotes image to primary.
|
||||
func (ri *rbdImage) promoteImage(force bool) error {
|
||||
// PromoteImage promotes image to primary.
|
||||
func (ri *rbdImage) PromoteImage(force bool) error {
|
||||
image, err := ri.open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open image %q with error: %w", ri, err)
|
||||
@ -88,10 +88,10 @@ func (ri *rbdImage) promoteImage(force bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// forcePromoteImage promotes image to primary with force option with 2 minutes
|
||||
// ForcePromoteImage promotes image to primary with force option with 2 minutes
|
||||
// timeout. If there is no response within 2 minutes,the rbd CLI process will be
|
||||
// killed and an error is returned.
|
||||
func (rv *rbdVolume) forcePromoteImage(cr *util.Credentials) error {
|
||||
func (rv *rbdVolume) ForcePromoteImage(cr *util.Credentials) error {
|
||||
promoteArgs := []string{
|
||||
"mirror", "image", "promote",
|
||||
rv.String(),
|
||||
@ -118,8 +118,8 @@ func (rv *rbdVolume) forcePromoteImage(cr *util.Credentials) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// demoteImage demotes image to secondary.
|
||||
func (ri *rbdImage) demoteImage() error {
|
||||
// DemoteImage demotes image to secondary.
|
||||
func (ri *rbdImage) DemoteImage() error {
|
||||
image, err := ri.open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open image %q with error: %w", ri, err)
|
||||
@ -148,8 +148,8 @@ func (ri *rbdImage) resyncImage() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getImageMirroringStatus get the mirroring status of an image.
|
||||
func (ri *rbdImage) getImageMirroringStatus() (*librbd.GlobalMirrorImageStatus, error) {
|
||||
// GetImageMirroringStatus get the mirroring status of an image.
|
||||
func (ri *rbdImage) GetImageMirroringStatus() (*librbd.GlobalMirrorImageStatus, error) {
|
||||
image, err := ri.open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open image %q with error: %w", ri, err)
|
||||
@ -163,8 +163,8 @@ func (ri *rbdImage) getImageMirroringStatus() (*librbd.GlobalMirrorImageStatus,
|
||||
return &statusInfo, nil
|
||||
}
|
||||
|
||||
// getLocalState returns the local state of the image.
|
||||
func (ri *rbdImage) getLocalState() (librbd.SiteMirrorImageStatus, error) {
|
||||
// GetLocalState returns the local state of the image.
|
||||
func (ri *rbdImage) GetLocalState() (librbd.SiteMirrorImageStatus, error) {
|
||||
localStatus := librbd.SiteMirrorImageStatus{}
|
||||
image, err := ri.open()
|
||||
if err != nil {
|
||||
|
@ -2018,7 +2018,7 @@ func (ri *rbdImage) isCompabitableClone(dst *rbdImage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ri *rbdImage) addSnapshotScheduling(
|
||||
func (ri *rbdImage) AddSnapshotScheduling(
|
||||
interval admin.Interval,
|
||||
startTime admin.StartTime,
|
||||
) error {
|
||||
|
133
internal/rbd/replication.go
Normal file
133
internal/rbd/replication.go
Normal file
@ -0,0 +1,133 @@
|
||||
/*
|
||||
Copyright 2023 The Ceph-CSI Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rbd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
librbd "github.com/ceph/go-ceph/rbd"
|
||||
"github.com/csi-addons/spec/lib/go/replication"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (rv *rbdVolume) ResyncVol(localStatus librbd.SiteMirrorImageStatus, force bool) error {
|
||||
if resyncRequired(localStatus) {
|
||||
// If the force option is not set return the error message to retry
|
||||
// with Force option.
|
||||
if !force {
|
||||
return status.Errorf(codes.FailedPrecondition,
|
||||
"image is in %q state, description (%s). Force resync to recover volume",
|
||||
localStatus.State, localStatus.Description)
|
||||
}
|
||||
err := rv.resyncImage()
|
||||
if err != nil {
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
// If we issued a resync, return a non-final error as image needs to be recreated
|
||||
// locally. Caller retries till RBD syncs an initial version of the image to
|
||||
// report its status in the resync request.
|
||||
return status.Error(codes.Unavailable, "awaiting initial resync due to split brain")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// repairResyncedImageID updates the existing image ID with new one.
|
||||
func (rv *rbdVolume) RepairResyncedImageID(ctx context.Context, ready bool) error {
|
||||
// During resync operation the local image will get deleted and a new
|
||||
// image is recreated by the rbd mirroring. The new image will have a
|
||||
// new image ID. Once resync is completed update the image ID in the OMAP
|
||||
// to get the image removed from the trash during DeleteVolume.
|
||||
|
||||
// if the image is not completely resynced skip repairing image ID.
|
||||
if !ready {
|
||||
return nil
|
||||
}
|
||||
j, err := volJournal.Connect(rv.Monitors, rv.RadosNamespace, rv.conn.Creds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer j.Destroy()
|
||||
// reset the image ID which is stored in the existing OMAP
|
||||
return rv.repairImageID(ctx, j, true)
|
||||
}
|
||||
|
||||
// resyncRequired returns true if local image is in split-brain state and image
|
||||
// needs resync.
|
||||
func resyncRequired(localStatus librbd.SiteMirrorImageStatus) bool {
|
||||
// resync is required if the image is in error state or the description
|
||||
// contains split-brain message.
|
||||
// In some corner cases like `re-player shutdown` the local image will not
|
||||
// be in an error state. It would be also worth considering the `description`
|
||||
// field to make sure about split-brain.
|
||||
if localStatus.State == librbd.MirrorImageStatusStateError ||
|
||||
strings.Contains(localStatus.Description, "split-brain") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func DisableVolumeReplication(rbdVol *rbdVolume,
|
||||
mirroringInfo *librbd.MirrorImageInfo,
|
||||
force bool,
|
||||
) (*replication.DisableVolumeReplicationResponse, error) {
|
||||
if !mirroringInfo.Primary {
|
||||
// Return success if the below condition is met
|
||||
// Local image is secondary
|
||||
// Local image is in up+replaying state
|
||||
|
||||
// If the image is in a secondary and its state is up+replaying means
|
||||
// its a healthy secondary and the image is primary somewhere in the
|
||||
// remote cluster and the local image is getting replayed. Return
|
||||
// success for the Disabling mirroring as we cannot disable mirroring
|
||||
// on the secondary image, when the image on the primary site gets
|
||||
// disabled the image on all the remote (secondary) clusters will get
|
||||
// auto-deleted. This helps in garbage collecting the volume
|
||||
// replication Kubernetes artifacts after failback operation.
|
||||
localStatus, rErr := rbdVol.GetLocalState()
|
||||
if rErr != nil {
|
||||
return nil, status.Error(codes.Internal, rErr.Error())
|
||||
}
|
||||
if localStatus.Up && localStatus.State == librbd.MirrorImageStatusStateReplaying {
|
||||
return &replication.DisableVolumeReplicationResponse{}, nil
|
||||
}
|
||||
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"secondary image status is up=%t and state=%s",
|
||||
localStatus.Up,
|
||||
localStatus.State)
|
||||
}
|
||||
err := rbdVol.DisableImageMirroring(force)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
// the image state can be still disabling once we disable the mirroring
|
||||
// check the mirroring is disabled or not
|
||||
mirroringInfo, err = rbdVol.GetImageMirroringInfo()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
if mirroringInfo.State == librbd.MirrorImageDisabling {
|
||||
return nil, status.Errorf(codes.Aborted, "%s is in disabling state", rbdVol.VolID)
|
||||
}
|
||||
|
||||
return &replication.DisableVolumeReplicationResponse{}, nil
|
||||
}
|
@ -1,920 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Ceph-CSI Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rbd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ceph/ceph-csi/internal/util"
|
||||
"github.com/ceph/ceph-csi/internal/util/log"
|
||||
|
||||
librbd "github.com/ceph/go-ceph/rbd"
|
||||
"github.com/ceph/go-ceph/rbd/admin"
|
||||
"github.com/csi-addons/spec/lib/go/replication"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// imageMirroringMode is used to indicate the mirroring mode for an RBD image.
|
||||
type imageMirroringMode string
|
||||
|
||||
const (
|
||||
// imageMirrorModeSnapshot uses snapshots to propagate RBD images between
|
||||
// ceph clusters.
|
||||
imageMirrorModeSnapshot imageMirroringMode = "snapshot"
|
||||
// imageMirrorModeJournal uses journaling to propagate RBD images between
|
||||
// ceph clusters.
|
||||
imageMirrorModeJournal imageMirroringMode = "journal"
|
||||
)
|
||||
|
||||
const (
|
||||
// mirroringMode + key to get the imageMirroringMode from parameters.
|
||||
imageMirroringKey = "mirroringMode"
|
||||
// forceKey + key to get the force option from parameters.
|
||||
forceKey = "force"
|
||||
|
||||
// schedulingIntervalKey to get the schedulingInterval from the
|
||||
// parameters.
|
||||
// Interval of time between scheduled snapshots. Typically in the form
|
||||
// <num><m,h,d>.
|
||||
schedulingIntervalKey = "schedulingInterval"
|
||||
|
||||
// schedulingStartTimeKey to get the schedulingStartTime from the
|
||||
// parameters.
|
||||
// (optional) StartTime is the time the snapshot schedule
|
||||
// begins, can be specified using the ISO 8601 time format.
|
||||
schedulingStartTimeKey = "schedulingStartTime"
|
||||
)
|
||||
|
||||
// ReplicationServer struct of rbd CSI driver with supported methods of Replication
|
||||
// controller server spec.
|
||||
type ReplicationServer struct {
|
||||
// added UnimplementedControllerServer as a member of
|
||||
// ControllerServer. if replication spec add more RPC services in the proto
|
||||
// file, then we don't need to add all RPC methods leading to forward
|
||||
// compatibility.
|
||||
*replication.UnimplementedControllerServer
|
||||
// Embed ControllerServer as it implements helper functions
|
||||
*ControllerServer
|
||||
}
|
||||
|
||||
func (rs *ReplicationServer) RegisterService(server grpc.ServiceRegistrar) {
|
||||
replication.RegisterControllerServer(server, rs)
|
||||
}
|
||||
|
||||
// getForceOption extracts the force option from the GRPC request parameters.
|
||||
// If not set, the default will be set to false.
|
||||
func getForceOption(ctx context.Context, parameters map[string]string) (bool, error) {
|
||||
val, ok := parameters[forceKey]
|
||||
if !ok {
|
||||
log.WarningLog(ctx, "%s is not set in parameters, setting to default (%v)", forceKey, false)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
force, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return false, status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return force, nil
|
||||
}
|
||||
|
||||
// getMirroringMode gets the mirroring mode from the input GRPC request parameters.
|
||||
// mirroringMode is the key to check the mode in the parameters.
|
||||
func getMirroringMode(ctx context.Context, parameters map[string]string) (librbd.ImageMirrorMode, error) {
|
||||
val, ok := parameters[imageMirroringKey]
|
||||
if !ok {
|
||||
log.WarningLog(
|
||||
ctx,
|
||||
"%s is not set in parameters, setting to mirroringMode to default (%s)",
|
||||
imageMirroringKey,
|
||||
imageMirrorModeSnapshot)
|
||||
|
||||
return librbd.ImageMirrorModeSnapshot, nil
|
||||
}
|
||||
|
||||
var mirroringMode librbd.ImageMirrorMode
|
||||
switch imageMirroringMode(val) {
|
||||
case imageMirrorModeSnapshot:
|
||||
mirroringMode = librbd.ImageMirrorModeSnapshot
|
||||
case imageMirrorModeJournal:
|
||||
mirroringMode = librbd.ImageMirrorModeJournal
|
||||
default:
|
||||
return mirroringMode, status.Errorf(codes.InvalidArgument, "%s %s not supported", imageMirroringKey, val)
|
||||
}
|
||||
|
||||
return mirroringMode, nil
|
||||
}
|
||||
|
||||
// validateSchedulingDetails gets the mirroring mode and scheduling details from the
|
||||
// input GRPC request parameters and validates the scheduling is only supported
|
||||
// for snapshot mirroring mode.
|
||||
func validateSchedulingDetails(ctx context.Context, parameters map[string]string) error {
|
||||
var err error
|
||||
|
||||
val := parameters[imageMirroringKey]
|
||||
|
||||
switch imageMirroringMode(val) {
|
||||
case imageMirrorModeJournal:
|
||||
// journal mirror mode does not require scheduling parameters
|
||||
if _, ok := parameters[schedulingIntervalKey]; ok {
|
||||
log.WarningLog(ctx, "%s parameter cannot be used with %s mirror mode, ignoring it",
|
||||
schedulingIntervalKey, string(imageMirrorModeJournal))
|
||||
}
|
||||
if _, ok := parameters[schedulingStartTimeKey]; ok {
|
||||
log.WarningLog(ctx, "%s parameter cannot be used with %s mirror mode, ignoring it",
|
||||
schedulingStartTimeKey, string(imageMirrorModeJournal))
|
||||
}
|
||||
|
||||
return nil
|
||||
case imageMirrorModeSnapshot:
|
||||
// If mirroring mode is not set in parameters, we are defaulting mirroring
|
||||
// mode to snapshot. Discard empty mirroring mode from validation as it is
|
||||
// an optional parameter.
|
||||
case "":
|
||||
default:
|
||||
return status.Error(codes.InvalidArgument, "scheduling is only supported for snapshot mode")
|
||||
}
|
||||
|
||||
// validate mandatory interval field
|
||||
interval, ok := parameters[schedulingIntervalKey]
|
||||
if ok && interval == "" {
|
||||
return status.Error(codes.InvalidArgument, "scheduling interval cannot be empty")
|
||||
}
|
||||
adminStartTime := admin.StartTime(parameters[schedulingStartTimeKey])
|
||||
if !ok {
|
||||
// startTime is alone not supported it has to be present with interval
|
||||
if adminStartTime != "" {
|
||||
return status.Errorf(codes.InvalidArgument,
|
||||
"%q parameter is supported only with %q",
|
||||
schedulingStartTimeKey,
|
||||
schedulingIntervalKey)
|
||||
}
|
||||
}
|
||||
if interval != "" {
|
||||
err = validateSchedulingInterval(interval)
|
||||
if err != nil {
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSchedulingDetails returns scheduling interval and scheduling startTime.
|
||||
func getSchedulingDetails(parameters map[string]string) (admin.Interval, admin.StartTime) {
|
||||
return admin.Interval(parameters[schedulingIntervalKey]),
|
||||
admin.StartTime(parameters[schedulingStartTimeKey])
|
||||
}
|
||||
|
||||
// validateSchedulingInterval return the interval as it is if its ending with
|
||||
// `m|h|d` or else it will return error.
|
||||
func validateSchedulingInterval(interval string) error {
|
||||
re := regexp.MustCompile(`^\d+[mhd]$`)
|
||||
if re.MatchString(interval) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("interval specified without d, h, m suffix")
|
||||
}
|
||||
|
||||
// EnableVolumeReplication extracts the RBD volume information from the
|
||||
// volumeID, If the image is present it will enable the mirroring based on the
|
||||
// user provided information.
|
||||
func (rs *ReplicationServer) EnableVolumeReplication(ctx context.Context,
|
||||
req *replication.EnableVolumeReplicationRequest,
|
||||
) (*replication.EnableVolumeReplicationResponse, error) {
|
||||
volumeID := req.GetVolumeId()
|
||||
if volumeID == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty volume ID in request")
|
||||
}
|
||||
cr, err := util.NewUserCredentials(req.GetSecrets())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
defer cr.DeleteCredentials()
|
||||
|
||||
err = validateSchedulingDetails(ctx, req.GetParameters())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if acquired := rs.VolumeLocks.TryAcquire(volumeID); !acquired {
|
||||
log.ErrorLog(ctx, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
|
||||
return nil, status.Errorf(codes.Aborted, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
}
|
||||
defer rs.VolumeLocks.Release(volumeID)
|
||||
|
||||
rbdVol, err := GenVolFromVolID(ctx, volumeID, cr, req.GetSecrets())
|
||||
defer rbdVol.Destroy()
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrImageNotFound):
|
||||
err = status.Errorf(codes.NotFound, "volume %s not found", volumeID)
|
||||
case errors.Is(err, util.ErrPoolNotFound):
|
||||
err = status.Errorf(codes.NotFound, "pool %s not found for %s", rbdVol.Pool, volumeID)
|
||||
default:
|
||||
err = status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
// extract the mirroring mode
|
||||
mirroringMode, err := getMirroringMode(ctx, req.GetParameters())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mirroringInfo, err := rbdVol.getImageMirroringInfo()
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
if mirroringInfo.State != librbd.MirrorImageEnabled {
|
||||
err = rbdVol.enableImageMirroring(mirroringMode)
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return &replication.EnableVolumeReplicationResponse{}, nil
|
||||
}
|
||||
|
||||
// DisableVolumeReplication extracts the RBD volume information from the
|
||||
// volumeID, If the image is present and the mirroring is enabled on the RBD
|
||||
// image it will disable the mirroring.
|
||||
func (rs *ReplicationServer) DisableVolumeReplication(ctx context.Context,
|
||||
req *replication.DisableVolumeReplicationRequest,
|
||||
) (*replication.DisableVolumeReplicationResponse, error) {
|
||||
volumeID := req.GetVolumeId()
|
||||
if volumeID == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty volume ID in request")
|
||||
}
|
||||
cr, err := util.NewUserCredentials(req.GetSecrets())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
defer cr.DeleteCredentials()
|
||||
|
||||
if acquired := rs.VolumeLocks.TryAcquire(volumeID); !acquired {
|
||||
log.ErrorLog(ctx, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
|
||||
return nil, status.Errorf(codes.Aborted, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
}
|
||||
defer rs.VolumeLocks.Release(volumeID)
|
||||
|
||||
rbdVol, err := GenVolFromVolID(ctx, volumeID, cr, req.GetSecrets())
|
||||
defer rbdVol.Destroy()
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrImageNotFound):
|
||||
err = status.Errorf(codes.NotFound, "volume %s not found", volumeID)
|
||||
case errors.Is(err, util.ErrPoolNotFound):
|
||||
err = status.Errorf(codes.NotFound, "pool %s not found for %s", rbdVol.Pool, volumeID)
|
||||
default:
|
||||
err = status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
// extract the force option
|
||||
force, err := getForceOption(ctx, req.GetParameters())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mirroringInfo, err := rbdVol.getImageMirroringInfo()
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
switch mirroringInfo.State {
|
||||
// image is already in disabled state
|
||||
case librbd.MirrorImageDisabled:
|
||||
// image mirroring is still disabling
|
||||
case librbd.MirrorImageDisabling:
|
||||
return nil, status.Errorf(codes.Aborted, "%s is in disabling state", volumeID)
|
||||
case librbd.MirrorImageEnabled:
|
||||
return disableVolumeReplication(rbdVol, mirroringInfo, force)
|
||||
default:
|
||||
return nil, status.Errorf(codes.InvalidArgument, "image is in %s Mode", mirroringInfo.State)
|
||||
}
|
||||
|
||||
return &replication.DisableVolumeReplicationResponse{}, nil
|
||||
}
|
||||
|
||||
func disableVolumeReplication(rbdVol *rbdVolume,
|
||||
mirroringInfo *librbd.MirrorImageInfo,
|
||||
force bool,
|
||||
) (*replication.DisableVolumeReplicationResponse, error) {
|
||||
if !mirroringInfo.Primary {
|
||||
// Return success if the below condition is met
|
||||
// Local image is secondary
|
||||
// Local image is in up+replaying state
|
||||
|
||||
// If the image is in a secondary and its state is up+replaying means
|
||||
// its a healthy secondary and the image is primary somewhere in the
|
||||
// remote cluster and the local image is getting replayed. Return
|
||||
// success for the Disabling mirroring as we cannot disable mirroring
|
||||
// on the secondary image, when the image on the primary site gets
|
||||
// disabled the image on all the remote (secondary) clusters will get
|
||||
// auto-deleted. This helps in garbage collecting the volume
|
||||
// replication Kubernetes artifacts after failback operation.
|
||||
localStatus, rErr := rbdVol.getLocalState()
|
||||
if rErr != nil {
|
||||
return nil, status.Error(codes.Internal, rErr.Error())
|
||||
}
|
||||
if localStatus.Up && localStatus.State == librbd.MirrorImageStatusStateReplaying {
|
||||
return &replication.DisableVolumeReplicationResponse{}, nil
|
||||
}
|
||||
|
||||
return nil, status.Errorf(codes.InvalidArgument,
|
||||
"secondary image status is up=%t and state=%s",
|
||||
localStatus.Up,
|
||||
localStatus.State)
|
||||
}
|
||||
err := rbdVol.disableImageMirroring(force)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
// the image state can be still disabling once we disable the mirroring
|
||||
// check the mirroring is disabled or not
|
||||
mirroringInfo, err = rbdVol.getImageMirroringInfo()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
if mirroringInfo.State == librbd.MirrorImageDisabling {
|
||||
return nil, status.Errorf(codes.Aborted, "%s is in disabling state", rbdVol.VolID)
|
||||
}
|
||||
|
||||
return &replication.DisableVolumeReplicationResponse{}, nil
|
||||
}
|
||||
|
||||
// PromoteVolume extracts the RBD volume information from the volumeID, If the
|
||||
// image is present, mirroring is enabled and the image is in demoted state it
|
||||
// will promote the volume as primary.
|
||||
// If the image is already primary it will return success.
|
||||
func (rs *ReplicationServer) PromoteVolume(ctx context.Context,
|
||||
req *replication.PromoteVolumeRequest,
|
||||
) (*replication.PromoteVolumeResponse, error) {
|
||||
volumeID := req.GetVolumeId()
|
||||
if volumeID == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty volume ID in request")
|
||||
}
|
||||
cr, err := util.NewUserCredentials(req.GetSecrets())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
defer cr.DeleteCredentials()
|
||||
|
||||
if acquired := rs.VolumeLocks.TryAcquire(volumeID); !acquired {
|
||||
log.ErrorLog(ctx, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
|
||||
return nil, status.Errorf(codes.Aborted, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
}
|
||||
defer rs.VolumeLocks.Release(volumeID)
|
||||
|
||||
rbdVol, err := GenVolFromVolID(ctx, volumeID, cr, req.GetSecrets())
|
||||
defer rbdVol.Destroy()
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrImageNotFound):
|
||||
err = status.Errorf(codes.NotFound, "volume %s not found", volumeID)
|
||||
case errors.Is(err, util.ErrPoolNotFound):
|
||||
err = status.Errorf(codes.NotFound, "pool %s not found for %s", rbdVol.Pool, volumeID)
|
||||
default:
|
||||
err = status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mirroringInfo, err := rbdVol.getImageMirroringInfo()
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
if mirroringInfo.State != librbd.MirrorImageEnabled {
|
||||
return nil, status.Errorf(
|
||||
codes.InvalidArgument,
|
||||
"mirroring is not enabled on %s, image is in %d Mode",
|
||||
rbdVol.VolID,
|
||||
mirroringInfo.State)
|
||||
}
|
||||
|
||||
// promote secondary to primary
|
||||
if !mirroringInfo.Primary {
|
||||
if req.GetForce() {
|
||||
// workaround for https://github.com/ceph/ceph-csi/issues/2736
|
||||
// TODO: remove this workaround when the issue is fixed
|
||||
err = rbdVol.forcePromoteImage(cr)
|
||||
} else {
|
||||
err = rbdVol.promoteImage(req.GetForce())
|
||||
}
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
// In case of the DR the image on the primary site cannot be
|
||||
// demoted as the cluster is down, during failover the image need
|
||||
// to be force promoted. RBD returns `Device or resource busy`
|
||||
// error message if the image cannot be promoted for above reason.
|
||||
// Return FailedPrecondition so that replication operator can send
|
||||
// request to force promote the image.
|
||||
if strings.Contains(err.Error(), "Device or resource busy") {
|
||||
return nil, status.Error(codes.FailedPrecondition, err.Error())
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
interval, startTime := getSchedulingDetails(req.GetParameters())
|
||||
if interval != admin.NoInterval {
|
||||
err = rbdVol.addSnapshotScheduling(interval, startTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.DebugLog(
|
||||
ctx,
|
||||
"Added scheduling at interval %s, start time %s for volume %s",
|
||||
interval,
|
||||
startTime,
|
||||
rbdVol)
|
||||
}
|
||||
|
||||
return &replication.PromoteVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
// DemoteVolume extracts the RBD volume information from the
|
||||
// volumeID, If the image is present, mirroring is enabled and the
|
||||
// image is in promoted state it will demote the volume as secondary.
|
||||
// If the image is already secondary it will return success.
|
||||
func (rs *ReplicationServer) DemoteVolume(ctx context.Context,
|
||||
req *replication.DemoteVolumeRequest,
|
||||
) (*replication.DemoteVolumeResponse, error) {
|
||||
volumeID := req.GetVolumeId()
|
||||
if volumeID == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty volume ID in request")
|
||||
}
|
||||
cr, err := util.NewUserCredentials(req.GetSecrets())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
defer cr.DeleteCredentials()
|
||||
|
||||
if acquired := rs.VolumeLocks.TryAcquire(volumeID); !acquired {
|
||||
log.ErrorLog(ctx, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
|
||||
return nil, status.Errorf(codes.Aborted, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
}
|
||||
defer rs.VolumeLocks.Release(volumeID)
|
||||
|
||||
rbdVol, err := GenVolFromVolID(ctx, volumeID, cr, req.GetSecrets())
|
||||
defer rbdVol.Destroy()
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrImageNotFound):
|
||||
err = status.Errorf(codes.NotFound, "volume %s not found", volumeID)
|
||||
case errors.Is(err, util.ErrPoolNotFound):
|
||||
err = status.Errorf(codes.NotFound, "pool %s not found for %s", rbdVol.Pool, volumeID)
|
||||
default:
|
||||
err = status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
mirroringInfo, err := rbdVol.getImageMirroringInfo()
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
if mirroringInfo.State != librbd.MirrorImageEnabled {
|
||||
return nil, status.Errorf(
|
||||
codes.InvalidArgument,
|
||||
"mirroring is not enabled on %s, image is in %d Mode",
|
||||
rbdVol.VolID,
|
||||
mirroringInfo.State)
|
||||
}
|
||||
|
||||
// demote image to secondary
|
||||
if mirroringInfo.Primary {
|
||||
err = rbdVol.demoteImage()
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return &replication.DemoteVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
// checkRemoteSiteStatus checks the state of the remote cluster.
|
||||
// It returns true if the state of the remote cluster is up and unknown.
|
||||
func checkRemoteSiteStatus(ctx context.Context, mirrorStatus *librbd.GlobalMirrorImageStatus) bool {
|
||||
ready := true
|
||||
found := false
|
||||
for _, s := range mirrorStatus.SiteStatuses {
|
||||
log.UsefulLog(
|
||||
ctx,
|
||||
"peer site mirrorUUID=%q, daemon up=%t, mirroring state=%q, description=%q and lastUpdate=%d",
|
||||
s.MirrorUUID,
|
||||
s.Up,
|
||||
s.State,
|
||||
s.Description,
|
||||
s.LastUpdate)
|
||||
if s.MirrorUUID != "" {
|
||||
found = true
|
||||
// If ready is already "false" do not flip it based on another remote peer status
|
||||
if ready && (s.State != librbd.MirrorImageStatusStateUnknown || !s.Up) {
|
||||
ready = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return readiness only if at least one remote peer status was processed
|
||||
return found && ready
|
||||
}
|
||||
|
||||
// ResyncVolume extracts the RBD volume information from the volumeID, If the
|
||||
// image is present, mirroring is enabled and the image is in demoted state.
|
||||
// If yes it will resync the image to correct the split-brain.
|
||||
func (rs *ReplicationServer) ResyncVolume(ctx context.Context,
|
||||
req *replication.ResyncVolumeRequest,
|
||||
) (*replication.ResyncVolumeResponse, error) {
|
||||
volumeID := req.GetVolumeId()
|
||||
if volumeID == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty volume ID in request")
|
||||
}
|
||||
cr, err := util.NewUserCredentials(req.GetSecrets())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
defer cr.DeleteCredentials()
|
||||
|
||||
if acquired := rs.VolumeLocks.TryAcquire(volumeID); !acquired {
|
||||
log.ErrorLog(ctx, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
|
||||
return nil, status.Errorf(codes.Aborted, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
}
|
||||
defer rs.VolumeLocks.Release(volumeID)
|
||||
rbdVol, err := GenVolFromVolID(ctx, volumeID, cr, req.GetSecrets())
|
||||
defer rbdVol.Destroy()
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrImageNotFound):
|
||||
err = status.Errorf(codes.NotFound, "volume %s not found", volumeID)
|
||||
case errors.Is(err, util.ErrPoolNotFound):
|
||||
err = status.Errorf(codes.NotFound, "pool %s not found for %s", rbdVol.Pool, volumeID)
|
||||
default:
|
||||
err = status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mirroringInfo, err := rbdVol.getImageMirroringInfo()
|
||||
if err != nil {
|
||||
// in case of Resync the image will get deleted and gets recreated and
|
||||
// it takes time for this operation.
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Aborted, err.Error())
|
||||
}
|
||||
|
||||
if mirroringInfo.State != librbd.MirrorImageEnabled {
|
||||
return nil, status.Error(codes.InvalidArgument, "image mirroring is not enabled")
|
||||
}
|
||||
|
||||
// return error if the image is still primary
|
||||
if mirroringInfo.Primary {
|
||||
return nil, status.Error(codes.InvalidArgument, "image is in primary state")
|
||||
}
|
||||
|
||||
mirrorStatus, err := rbdVol.getImageMirroringStatus()
|
||||
if err != nil {
|
||||
// the image gets recreated after issuing resync
|
||||
if errors.Is(err, ErrImageNotFound) {
|
||||
// caller retries till RBD syncs an initial version of the image to
|
||||
// report its status in the resync call. Ideally, this line will not
|
||||
// be executed as the error would get returned due to getImageMirroringInfo
|
||||
// failing to find an image above.
|
||||
return nil, status.Error(codes.Aborted, err.Error())
|
||||
}
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
ready := false
|
||||
|
||||
localStatus, err := mirrorStatus.LocalStatus()
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, fmt.Errorf("failed to get local status: %w", err)
|
||||
}
|
||||
|
||||
// convert the last update time to UTC
|
||||
lastUpdateTime := time.Unix(localStatus.LastUpdate, 0).UTC()
|
||||
log.UsefulLog(
|
||||
ctx,
|
||||
"local status: daemon up=%t, image mirroring state=%q, description=%q and lastUpdate=%s",
|
||||
localStatus.Up,
|
||||
localStatus.State,
|
||||
localStatus.Description,
|
||||
lastUpdateTime)
|
||||
|
||||
// To recover from split brain (up+error) state the image need to be
|
||||
// demoted and requested for resync on site-a and then the image on site-b
|
||||
// should be demoted. The volume should be marked to ready=true when the
|
||||
// image state on both the clusters are up+unknown because during the last
|
||||
// snapshot syncing the data gets copied first and then image state on the
|
||||
// site-a changes to up+unknown.
|
||||
|
||||
// If the image state on both the sites are up+unknown consider that
|
||||
// complete data is synced as the last snapshot
|
||||
// gets exchanged between the clusters.
|
||||
if localStatus.State == librbd.MirrorImageStatusStateUnknown && localStatus.Up {
|
||||
ready = checkRemoteSiteStatus(ctx, mirrorStatus)
|
||||
}
|
||||
|
||||
err = resyncVolume(localStatus, rbdVol, req.Force)
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = checkVolumeResyncStatus(localStatus)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
err = repairResyncedImageID(ctx, rbdVol, ready)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to resync Image ID: %s", err.Error())
|
||||
}
|
||||
|
||||
resp := &replication.ResyncVolumeResponse{
|
||||
Ready: ready,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetVolumeReplicationInfo extracts the RBD volume information from the volumeID, If the
|
||||
// image is present, mirroring is enabled and the image is in primary state.
|
||||
func (rs *ReplicationServer) GetVolumeReplicationInfo(ctx context.Context,
|
||||
req *replication.GetVolumeReplicationInfoRequest,
|
||||
) (*replication.GetVolumeReplicationInfoResponse, error) {
|
||||
volumeID := req.GetVolumeId()
|
||||
if volumeID == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "empty volume ID in request")
|
||||
}
|
||||
cr, err := util.NewUserCredentials(req.GetSecrets())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
defer cr.DeleteCredentials()
|
||||
|
||||
if acquired := rs.VolumeLocks.TryAcquire(volumeID); !acquired {
|
||||
log.ErrorLog(ctx, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
|
||||
return nil, status.Errorf(codes.Aborted, util.VolumeOperationAlreadyExistsFmt, volumeID)
|
||||
}
|
||||
defer rs.VolumeLocks.Release(volumeID)
|
||||
rbdVol, err := GenVolFromVolID(ctx, volumeID, cr, req.GetSecrets())
|
||||
defer rbdVol.Destroy()
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrImageNotFound):
|
||||
err = status.Errorf(codes.NotFound, "volume %s not found", volumeID)
|
||||
case errors.Is(err, util.ErrPoolNotFound):
|
||||
err = status.Errorf(codes.NotFound, "pool %s not found for %s", rbdVol.Pool, volumeID)
|
||||
default:
|
||||
err = status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mirroringInfo, err := rbdVol.getImageMirroringInfo()
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Aborted, err.Error())
|
||||
}
|
||||
|
||||
if mirroringInfo.State != librbd.MirrorImageEnabled {
|
||||
return nil, status.Error(codes.InvalidArgument, "image mirroring is not enabled")
|
||||
}
|
||||
|
||||
// return error if the image is not in primary state
|
||||
if !mirroringInfo.Primary {
|
||||
return nil, status.Error(codes.InvalidArgument, "image is not in primary state")
|
||||
}
|
||||
|
||||
mirrorStatus, err := rbdVol.getImageMirroringStatus()
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrImageNotFound) {
|
||||
return nil, status.Error(codes.Aborted, err.Error())
|
||||
}
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
remoteStatus, err := RemoteStatus(mirrorStatus)
|
||||
if err != nil {
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Errorf(codes.Internal, "failed to get remote status: %v", err)
|
||||
}
|
||||
|
||||
description := remoteStatus.Description
|
||||
lastSyncTime, err := getLastSyncTime(description)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrLastSyncTimeNotFound) {
|
||||
return nil, status.Errorf(codes.NotFound, "failed to get last sync time: %v", err)
|
||||
}
|
||||
log.ErrorLog(ctx, err.Error())
|
||||
|
||||
return nil, status.Errorf(codes.Internal, "failed to get last sync time: %v", err)
|
||||
}
|
||||
|
||||
resp := &replication.GetVolumeReplicationInfoResponse{
|
||||
LastSyncTime: lastSyncTime,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// RemoteStatus returns one SiteMirrorImageStatus item from the SiteStatuses
|
||||
// slice that corresponds to the remote site's status. If the remote status
|
||||
// is not found than the error ErrNotExist will be returned.
|
||||
func RemoteStatus(gmis *librbd.GlobalMirrorImageStatus) (librbd.SiteMirrorImageStatus, error) {
|
||||
var (
|
||||
ss librbd.SiteMirrorImageStatus
|
||||
err error = librbd.ErrNotExist
|
||||
)
|
||||
for i := range gmis.SiteStatuses {
|
||||
if gmis.SiteStatuses[i].MirrorUUID != "" {
|
||||
ss = gmis.SiteStatuses[i]
|
||||
err = nil
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ss, err
|
||||
}
|
||||
|
||||
// This function gets the local snapshot time from the description
|
||||
// of localStatus and converts it into required type.
|
||||
func getLastSyncTime(description string) (*timestamppb.Timestamp, error) {
|
||||
// Format of the description will be as followed:
|
||||
// description = "replaying,{"bytes_per_second":0.0,
|
||||
// "bytes_per_snapshot":149504.0,"local_snapshot_timestamp":1662655501
|
||||
// ,"remote_snapshot_timestamp":1662655501}"
|
||||
// In case there is no local snapshot timestamp return an error as the
|
||||
// LastSyncTime is required.
|
||||
if description == "" {
|
||||
return nil, fmt.Errorf("empty description: %w", ErrLastSyncTimeNotFound)
|
||||
}
|
||||
splittedString := strings.SplitN(description, ",", 2)
|
||||
if len(splittedString) == 1 {
|
||||
return nil, fmt.Errorf("no local snapshot timestamp: %w", ErrLastSyncTimeNotFound)
|
||||
}
|
||||
type localStatus struct {
|
||||
LocalSnapshotTime int64 `json:"local_snapshot_timestamp"`
|
||||
}
|
||||
|
||||
var localSnapTime localStatus
|
||||
err := json.Unmarshal([]byte(splittedString[1]), &localSnapTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal description: %w", err)
|
||||
}
|
||||
|
||||
// If the json unmarsal is successful but the local snapshot time is 0, we
|
||||
// need to consider it as an error as the LastSyncTime is required.
|
||||
if localSnapTime.LocalSnapshotTime == 0 {
|
||||
return nil, fmt.Errorf("empty local snapshot timestamp: %w", ErrLastSyncTimeNotFound)
|
||||
}
|
||||
|
||||
lastUpdateTime := time.Unix(localSnapTime.LocalSnapshotTime, 0)
|
||||
lastSyncTime := timestamppb.New(lastUpdateTime)
|
||||
|
||||
return lastSyncTime, nil
|
||||
}
|
||||
|
||||
func resyncVolume(localStatus librbd.SiteMirrorImageStatus, rbdVol *rbdVolume, force bool) error {
|
||||
if resyncRequired(localStatus) {
|
||||
// If the force option is not set return the error message to retry
|
||||
// with Force option.
|
||||
if !force {
|
||||
return status.Errorf(codes.FailedPrecondition,
|
||||
"image is in %q state, description (%s). Force resync to recover volume",
|
||||
localStatus.State, localStatus.Description)
|
||||
}
|
||||
err := rbdVol.resyncImage()
|
||||
if err != nil {
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
// If we issued a resync, return a non-final error as image needs to be recreated
|
||||
// locally. Caller retries till RBD syncs an initial version of the image to
|
||||
// report its status in the resync request.
|
||||
return status.Error(codes.Unavailable, "awaiting initial resync due to split brain")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkVolumeResyncStatus(localStatus librbd.SiteMirrorImageStatus) error {
|
||||
// we are considering 2 states to check resync started and resync completed
|
||||
// as below. all other states will be considered as an error state so that
|
||||
// cephCSI can return error message and volume replication operator can
|
||||
// mark the VolumeReplication status as not resyncing for the volume.
|
||||
|
||||
// If the state is Replaying means the resync is going on.
|
||||
// Once the volume on remote cluster is demoted and resync
|
||||
// is completed the image state will be moved to UNKNOWN.
|
||||
// RBD mirror daemon should be always running on the primary cluster.
|
||||
if !localStatus.Up || (localStatus.State != librbd.MirrorImageStatusStateReplaying &&
|
||||
localStatus.State != librbd.MirrorImageStatusStateUnknown) {
|
||||
return fmt.Errorf(
|
||||
"not resyncing. Local status: daemon up=%t image is in %q state",
|
||||
localStatus.Up, localStatus.State)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resyncRequired returns true if local image is in split-brain state and image
|
||||
// needs resync.
|
||||
func resyncRequired(localStatus librbd.SiteMirrorImageStatus) bool {
|
||||
// resync is required if the image is in error state or the description
|
||||
// contains split-brain message.
|
||||
// In some corner cases like `re-player shutdown` the local image will not
|
||||
// be in an error state. It would be also worth considering the `description`
|
||||
// field to make sure about split-brain.
|
||||
if localStatus.State == librbd.MirrorImageStatusStateError ||
|
||||
strings.Contains(localStatus.Description, "split-brain") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// repairResyncedImageID updates the existing image ID with new one.
|
||||
func repairResyncedImageID(ctx context.Context, rv *rbdVolume, ready bool) error {
|
||||
// During resync operation the local image will get deleted and a new
|
||||
// image is recreated by the rbd mirroring. The new image will have a
|
||||
// new image ID. Once resync is completed update the image ID in the OMAP
|
||||
// to get the image removed from the trash during DeleteVolume.
|
||||
|
||||
// if the image is not completely resynced skip repairing image ID.
|
||||
if !ready {
|
||||
return nil
|
||||
}
|
||||
j, err := volJournal.Connect(rv.Monitors, rv.RadosNamespace, rv.conn.Creds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer j.Destroy()
|
||||
// reset the image ID which is stored in the existing OMAP
|
||||
return rv.repairImageID(ctx, j, true)
|
||||
}
|
@ -1,494 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Ceph-CSI Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package rbd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
librbd "github.com/ceph/go-ceph/rbd"
|
||||
"github.com/ceph/go-ceph/rbd/admin"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestValidateSchedulingInterval(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
interval string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"valid interval in minutes",
|
||||
"3m",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"valid interval in hour",
|
||||
"22h",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"valid interval in days",
|
||||
"13d",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid interval without number",
|
||||
"d",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"invalid interval without (m|h|d) suffix",
|
||||
"12",
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := validateSchedulingInterval(tt.interval)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateSchedulingInterval() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSchedulingDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
tests := []struct {
|
||||
name string
|
||||
parameters map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"valid parameters",
|
||||
map[string]string{
|
||||
imageMirroringKey: string(imageMirrorModeSnapshot),
|
||||
schedulingIntervalKey: "1h",
|
||||
schedulingStartTimeKey: "14:00:00-05:00",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"valid parameters when optional startTime is missing",
|
||||
map[string]string{
|
||||
imageMirroringKey: string(imageMirrorModeSnapshot),
|
||||
schedulingIntervalKey: "1h",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"when mirroring mode is journal",
|
||||
map[string]string{
|
||||
imageMirroringKey: string(imageMirrorModeJournal),
|
||||
schedulingIntervalKey: "1h",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"when startTime is specified without interval",
|
||||
map[string]string{
|
||||
imageMirroringKey: string(imageMirrorModeSnapshot),
|
||||
schedulingStartTimeKey: "14:00:00-05:00",
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"when no scheduling is specified",
|
||||
map[string]string{
|
||||
imageMirroringKey: string(imageMirrorModeSnapshot),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"when no parameters and scheduling details are specified",
|
||||
map[string]string{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"when no mirroring mode is specified",
|
||||
map[string]string{
|
||||
schedulingIntervalKey: "1h",
|
||||
schedulingStartTimeKey: "14:00:00-05:00",
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := validateSchedulingDetails(ctx, tt.parameters)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getSchedulingDetails() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSchedulingDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
parameters map[string]string
|
||||
wantInterval admin.Interval
|
||||
wantStartTime admin.StartTime
|
||||
}{
|
||||
{
|
||||
"valid parameters",
|
||||
map[string]string{
|
||||
schedulingIntervalKey: "1h",
|
||||
schedulingStartTimeKey: "14:00:00-05:00",
|
||||
},
|
||||
admin.Interval("1h"),
|
||||
admin.StartTime("14:00:00-05:00"),
|
||||
},
|
||||
{
|
||||
"valid parameters when optional startTime is missing",
|
||||
map[string]string{
|
||||
imageMirroringKey: string(imageMirrorModeSnapshot),
|
||||
schedulingIntervalKey: "1h",
|
||||
},
|
||||
admin.Interval("1h"),
|
||||
admin.NoStartTime,
|
||||
},
|
||||
{
|
||||
"when startTime is specified without interval",
|
||||
map[string]string{
|
||||
imageMirroringKey: string(imageMirrorModeSnapshot),
|
||||
schedulingStartTimeKey: "14:00:00-05:00",
|
||||
},
|
||||
admin.NoInterval,
|
||||
admin.StartTime("14:00:00-05:00"),
|
||||
},
|
||||
{
|
||||
"when no parameters and scheduling details are specified",
|
||||
map[string]string{},
|
||||
admin.NoInterval,
|
||||
admin.NoStartTime,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
interval, startTime := getSchedulingDetails(tt.parameters)
|
||||
if !reflect.DeepEqual(interval, tt.wantInterval) {
|
||||
t.Errorf("getSchedulingDetails() interval = %v, want %v", interval, tt.wantInterval)
|
||||
}
|
||||
if !reflect.DeepEqual(startTime, tt.wantStartTime) {
|
||||
t.Errorf("getSchedulingDetails() startTime = %v, want %v", startTime, tt.wantStartTime)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckVolumeResyncStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
args librbd.SiteMirrorImageStatus
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "test when rbd mirror daemon is not running",
|
||||
args: librbd.SiteMirrorImageStatus{
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: false,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "test for unknown state",
|
||||
args: librbd.SiteMirrorImageStatus{
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "test for error state",
|
||||
args: librbd.SiteMirrorImageStatus{
|
||||
State: librbd.MirrorImageStatusStateError,
|
||||
Up: true,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "test for syncing state",
|
||||
args: librbd.SiteMirrorImageStatus{
|
||||
State: librbd.MirrorImageStatusStateSyncing,
|
||||
Up: true,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "test for starting_replay state",
|
||||
args: librbd.SiteMirrorImageStatus{
|
||||
State: librbd.MirrorImageStatusStateStartingReplay,
|
||||
Up: true,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "test for replaying state",
|
||||
args: librbd.SiteMirrorImageStatus{
|
||||
State: librbd.MirrorImageStatusStateReplaying,
|
||||
Up: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "test for stopping_replay state",
|
||||
args: librbd.SiteMirrorImageStatus{
|
||||
State: librbd.MirrorImageStatusStateStoppingReplay,
|
||||
Up: true,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "test for stopped state",
|
||||
args: librbd.SiteMirrorImageStatus{
|
||||
State: librbd.MirrorImageStatusStateStopped,
|
||||
Up: true,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "test for invalid state",
|
||||
args: librbd.SiteMirrorImageStatus{
|
||||
State: librbd.MirrorImageStatusState(100),
|
||||
Up: true,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ts := tt
|
||||
t.Run(ts.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := checkVolumeResyncStatus(ts.args); (err != nil) != ts.wantErr {
|
||||
t.Errorf("checkVolumeResyncStatus() error = %v, expect error = %v", err, ts.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRemoteSiteStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
args librbd.GlobalMirrorImageStatus
|
||||
wantReady bool
|
||||
}{
|
||||
{
|
||||
name: "Test a single peer in sync",
|
||||
args: librbd.GlobalMirrorImageStatus{
|
||||
SiteStatuses: []librbd.SiteMirrorImageStatus{
|
||||
{
|
||||
MirrorUUID: "remote",
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantReady: true,
|
||||
},
|
||||
{
|
||||
name: "Test a single peer in sync, including a local instance",
|
||||
args: librbd.GlobalMirrorImageStatus{
|
||||
SiteStatuses: []librbd.SiteMirrorImageStatus{
|
||||
{
|
||||
MirrorUUID: "remote",
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: true,
|
||||
},
|
||||
{
|
||||
MirrorUUID: "",
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantReady: true,
|
||||
},
|
||||
{
|
||||
name: "Test a multiple peers in sync",
|
||||
args: librbd.GlobalMirrorImageStatus{
|
||||
SiteStatuses: []librbd.SiteMirrorImageStatus{
|
||||
{
|
||||
MirrorUUID: "remote1",
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: true,
|
||||
},
|
||||
{
|
||||
MirrorUUID: "remote2",
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantReady: true,
|
||||
},
|
||||
{
|
||||
name: "Test no remote peers",
|
||||
args: librbd.GlobalMirrorImageStatus{
|
||||
SiteStatuses: []librbd.SiteMirrorImageStatus{},
|
||||
},
|
||||
wantReady: false,
|
||||
},
|
||||
{
|
||||
name: "Test single peer not in sync",
|
||||
args: librbd.GlobalMirrorImageStatus{
|
||||
SiteStatuses: []librbd.SiteMirrorImageStatus{
|
||||
{
|
||||
MirrorUUID: "remote",
|
||||
State: librbd.MirrorImageStatusStateReplaying,
|
||||
Up: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantReady: false,
|
||||
},
|
||||
{
|
||||
name: "Test single peer not up",
|
||||
args: librbd.GlobalMirrorImageStatus{
|
||||
SiteStatuses: []librbd.SiteMirrorImageStatus{
|
||||
{
|
||||
MirrorUUID: "remote",
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantReady: false,
|
||||
},
|
||||
{
|
||||
name: "Test multiple peers, when first peer is not in sync",
|
||||
args: librbd.GlobalMirrorImageStatus{
|
||||
SiteStatuses: []librbd.SiteMirrorImageStatus{
|
||||
{
|
||||
MirrorUUID: "remote1",
|
||||
State: librbd.MirrorImageStatusStateStoppingReplay,
|
||||
Up: true,
|
||||
},
|
||||
{
|
||||
MirrorUUID: "remote2",
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantReady: false,
|
||||
},
|
||||
{
|
||||
name: "Test multiple peers, when second peer is not up",
|
||||
args: librbd.GlobalMirrorImageStatus{
|
||||
SiteStatuses: []librbd.SiteMirrorImageStatus{
|
||||
{
|
||||
MirrorUUID: "remote1",
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: true,
|
||||
},
|
||||
{
|
||||
MirrorUUID: "remote2",
|
||||
State: librbd.MirrorImageStatusStateUnknown,
|
||||
Up: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantReady: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ts := tt
|
||||
t.Run(ts.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if ready := checkRemoteSiteStatus(context.TODO(), &ts.args); ready != ts.wantReady {
|
||||
t.Errorf("checkRemoteSiteStatus() ready = %v, expect ready = %v", ready, ts.wantReady)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLastSyncTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
description string
|
||||
timestamp *timestamppb.Timestamp
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
"valid description",
|
||||
//nolint:lll // sample output cannot be split into multiple lines.
|
||||
`replaying,{"bytes_per_second":0.0,"bytes_per_snapshot":149504.0,"local_snapshot_timestamp":1662655501,"remote_snapshot_timestamp":1662655501}`,
|
||||
timestamppb.New(time.Unix(1662655501, 0)),
|
||||
"",
|
||||
},
|
||||
{
|
||||
"empty description",
|
||||
"",
|
||||
nil,
|
||||
ErrLastSyncTimeNotFound.Error(),
|
||||
},
|
||||
{
|
||||
"description without local_snapshot_timestamp",
|
||||
`replaying,{"bytes_per_second":0.0,"bytes_per_snapshot":149504.0,"remote_snapshot_timestamp":1662655501}`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"description with invalid JSON",
|
||||
`replaying,{"bytes_per_second":0.0,"bytes_per_snapshot":149504.0","remote_snapshot_timestamp":1662655501`,
|
||||
nil,
|
||||
"failed to unmarshal",
|
||||
},
|
||||
{
|
||||
"description with no JSON",
|
||||
`replaying`,
|
||||
nil,
|
||||
ErrLastSyncTimeNotFound.Error(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts, err := getLastSyncTime(tt.description)
|
||||
if err != nil && !strings.Contains(err.Error(), tt.expectedErr) {
|
||||
// returned error
|
||||
t.Errorf("getLastSyncTime() returned error, expected: %v, got: %v",
|
||||
tt.expectedErr, err)
|
||||
}
|
||||
if !ts.AsTime().Equal(tt.timestamp.AsTime()) {
|
||||
t.Errorf("getLastSyncTime() %v, expected %v", ts, tt.timestamp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user