feat(linstor): bump linstor-csi to v1.10.6 with Protocol-C dual-attach fix

Bump linstor-csi to v1.10.6 and add an out-of-tree patch that overrides
DrbdOptions/Net/protocol to C on the resource-definition whenever a
volume is attached to a second node with allow-two-primaries (the
live-migration flow), and reverts it on detach.

Without the patch, any KubeVirt live migration of a VM whose volume
sits on a Protocol-A/B resource (for example a "replicated-async"
StorageClass) ends up in a permanent evacuation loop: drbdadm adjust
on the satellite rejects allow-two-primaries with "Protocol C
required" (errno 139), the migration fails, and KubeVirt retries
until manual intervention.

The override is set at the resource-definition level so it covers
every connection, including diskless TieBreaker peers, and is tagged
with an Aux marker so Detach reverts only the override we installed.
The 001 relocate-after-clone patch was regenerated against v1.10.6
(context shift only, no logic change).

Verified end-to-end on dev5: live migration of a KubeVirt VM whose
PVC sits on a Protocol-A resource-group now succeeds in a single
Attach.

Patch is upstreamed as draft PR piraeusdatastore/linstor-csi#435.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
Andrei Kvapil 2026-04-27 11:32:17 +02:00
parent 52edffbb9a
commit 073fb1630d
No known key found for this signature in database
GPG key ID: 931CF7FEACEAF765
4 changed files with 140 additions and 58 deletions

View file

@ -5,7 +5,7 @@ include ../../../hack/common-envs.mk
include ../../../hack/package.mk
LINSTOR_VERSION ?= 1.33.2
LINSTOR_CSI_VERSION ?= v1.10.5
LINSTOR_CSI_VERSION ?= v1.10.6
image: image-piraeus-server image-linstor-csi

View file

@ -1,6 +1,6 @@
FROM golang:1.25 AS builder
ARG VERSION=v1.10.5
ARG VERSION=v1.10.6
ARG LINSTOR_WAIT_UNTIL_VERSION=v0.3.1
ARG TARGETARCH
ARG TARGETOS

View file

@ -1,25 +1,7 @@
diff --git a/pkg/client/linstor.go b/pkg/client/linstor.go
index f544493..98e7fde 100644
index ac4651d..46d565d 100644
--- a/pkg/client/linstor.go
+++ b/pkg/client/linstor.go
@@ -181,7 +181,7 @@ func LogLevel(s string) func(*Linstor) error {
func (s *Linstor) ListAllWithStatus(ctx context.Context) ([]volume.VolumeStatus, error) {
var vols []volume.VolumeStatus
- resourcesByName := make(map[string][]lapi.Resource)
+ resourcesByName := make(map[string][]lapi.ResourceWithVolumes)
resDefs, err := s.client.ResourceDefinitions.GetAll(ctx, lapi.RDGetAllRequest{WithVolumeDefinitions: true})
if err != nil {
@@ -194,7 +194,7 @@ func (s *Linstor) ListAllWithStatus(ctx context.Context) ([]volume.VolumeStatus,
}
for i := range allResources {
- resourcesByName[allResources[i].Name] = append(resourcesByName[allResources[i].Name], allResources[i].Resource)
+ resourcesByName[allResources[i].Name] = append(resourcesByName[allResources[i].Name], allResources[i])
}
for _, rd := range resDefs {
@@ -462,6 +462,14 @@ func (s *Linstor) Clone(ctx context.Context, vol, src *volume.Info, params *volu
return err
}
@ -35,7 +17,7 @@ index f544493..98e7fde 100644
logger.Debug("reconcile extra properties")
err = s.client.ResourceDefinitions.Modify(ctx, vol.ID, lapi.GenericPropsModify{OverrideProps: vol.Properties})
@@ -1280,6 +1288,14 @@ func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *v
@@ -1297,6 +1305,14 @@ func (s *Linstor) VolFromSnap(ctx context.Context, snap *volume.Snapshot, vol *v
return err
}
@ -50,7 +32,7 @@ index f544493..98e7fde 100644
logger.Debug("reconcile extra properties")
err = s.client.ResourceDefinitions.Modify(ctx, vol.ID, lapi.GenericPropsModify{OverrideProps: vol.Properties})
@@ -1470,9 +1486,8 @@ func (s *Linstor) reconcileSnapshotResources(ctx context.Context, snapshot *volu
@@ -1487,9 +1503,8 @@ func (s *Linstor) reconcileSnapshotResources(ctx context.Context, snapshot *volu
return fmt.Errorf("snapshot '%s' not deployed on any node", snap.Name)
}
@ -62,7 +44,7 @@ index f544493..98e7fde 100644
for _, snapNode := range snap.Nodes {
if err := s.NodeAvailable(ctx, snapNode); err != nil {
logger.WithField("selected node candidate", snapNode).WithError(err).Debug("node is not available")
@@ -1480,13 +1495,23 @@ func (s *Linstor) reconcileSnapshotResources(ctx context.Context, snapshot *volu
@@ -1497,13 +1512,23 @@ func (s *Linstor) reconcileSnapshotResources(ctx context.Context, snapshot *volu
}
if slices.Contains(preferredNodes, snapNode) {
@ -92,7 +74,7 @@ index f544493..98e7fde 100644
}
if selectedNode == "" {
@@ -1679,6 +1704,114 @@ func (s *Linstor) reconcileResourcePlacement(ctx context.Context, vol *volume.In
@@ -1696,6 +1721,114 @@ func (s *Linstor) reconcileResourcePlacement(ctx context.Context, vol *volume.In
return nil
}
@ -207,40 +189,8 @@ index f544493..98e7fde 100644
// FindSnapsByID searches the snapshot in the backend
func (s *Linstor) FindSnapsByID(ctx context.Context, id string) ([]*volume.Snapshot, error) {
snapshotId, err := volume.ParseSnapshotId(id)
@@ -2173,7 +2306,7 @@ func (s *Linstor) Status(ctx context.Context, volId string) ([]string, *csi.Volu
"volume": volId,
}).Debug("getting assignments")
- ress, err := s.client.Resources.GetAll(ctx, volId)
+ ress, err := s.client.Resources.GetResourceView(ctx, &lapi.ListOpts{Resource: []string{volId}})
if err != nil {
return nil, nil, fmt.Errorf("failed to list resources for '%s': %w", volId, err)
}
@@ -2261,13 +2394,20 @@ func GetSnapshotRemoteAndReadiness(snap *lapi.Snapshot) (string, bool, error) {
return "", slices.Contains(snap.Flags, lapiconsts.FlagSuccessful), nil
}
-func NodesAndConditionFromResources(ress []lapi.Resource) ([]string, *csi.VolumeCondition) {
+func NodesAndConditionFromResources(ress []lapi.ResourceWithVolumes) ([]string, *csi.VolumeCondition) {
var allNodes, abnormalNodes []string
for i := range ress {
res := &ress[i]
- allNodes = append(allNodes, res.NodeName)
+ // A resource is a CSI publish target if any of its volumes were created
+ // by ControllerPublishVolume, identified by the temporary-diskless-attach property.
+ if slices.ContainsFunc(res.Volumes, func(v lapi.Volume) bool {
+ createdFor, ok := v.Props[linstor.PropertyCreatedFor]
+ return ok && createdFor == linstor.CreatedForTemporaryDisklessAttach
+ }) {
+ allNodes = append(allNodes, res.NodeName)
+ }
if res.State == nil {
abnormalNodes = append(abnormalNodes, res.NodeName)
diff --git a/pkg/volume/parameter.go b/pkg/volume/parameter.go
index 39acd95..aed18ab 100644
index 39acd95..54d1dfb 100644
--- a/pkg/volume/parameter.go
+++ b/pkg/volume/parameter.go
@@ -50,6 +50,7 @@ const (

View file

@ -0,0 +1,132 @@
diff --git a/pkg/client/linstor.go b/pkg/client/linstor.go
index ac4651d..9d75c79 100644
--- a/pkg/client/linstor.go
+++ b/pkg/client/linstor.go
@@ -653,11 +653,35 @@ func (s *Linstor) Attach(ctx context.Context, volId, node string, rwxBlock bool)
}
if otherResInUse > 0 && rwxBlock {
- rdPropsModify := lapi.GenericPropsModify{OverrideProps: map[string]string{
+ rdProps := map[string]string{
linstor.PropertyAllowTwoPrimaries: "yes",
- }}
+ }
- err = s.client.ResourceDefinitions.Modify(ctx, volId, rdPropsModify)
+ // DRBD requires Protocol C whenever allow-two-primaries is enabled.
+ // If the resource is configured with Protocol A or B (typically
+ // through its resource-group), drbdadm adjust on the satellites
+ // fails with "Protocol C required" (errno 139) and live migration
+ // cannot proceed. Override Protocol to C on the resource-definition
+ // itself so it applies to every connection (including diskless
+ // peers, e.g. a TieBreaker). The marker lets Detach revert exactly
+ // the override we installed without disturbing operator-set props.
+ proto, protoErr := s.getEffectiveDrbdProtocol(ctx, volId)
+ if protoErr != nil {
+ s.log.WithError(protoErr).WithField("volume", volId).
+ Warn("failed to determine effective DRBD protocol; skipping Protocol=C override for dual-attach")
+ } else if proto == "A" || proto == "B" {
+ s.log.WithFields(logrus.Fields{
+ "volume": volId,
+ "protocol": proto,
+ }).Info("installing Protocol=C override on resource-definition for dual-attach")
+
+ rdProps[linstor.PropertyDrbdNetProtocol] = "C"
+ rdProps[linstor.PropertyCsiProtocolOverride] = "yes"
+ }
+
+ err = s.client.ResourceDefinitions.Modify(ctx, volId, lapi.GenericPropsModify{
+ OverrideProps: rdProps,
+ })
if err != nil {
return "", err
}
@@ -774,11 +798,26 @@ func (s *Linstor) Detach(ctx context.Context, volId, node string) error {
}
if resInUse == 1 {
- rdPropsModify := lapi.GenericPropsModify{DeleteProps: []string{
- linstor.PropertyAllowTwoPrimaries,
- }}
+ deleteProps := []string{linstor.PropertyAllowTwoPrimaries}
+
+ // Drop the Protocol=C override only if Attach installed it (gated by
+ // our marker), so an operator-set Protocol property on the
+ // resource-definition is preserved.
+ rd, getErr := s.client.ResourceDefinitions.Get(ctx, volId)
+ if getErr != nil {
+ log.WithError(getErr).Warn("failed to fetch resource-definition props; skipping Protocol=C override removal")
+ } else if rd.Props[linstor.PropertyCsiProtocolOverride] != "" {
+ log.Info("removing Protocol=C override from resource-definition")
- err = s.client.ResourceDefinitions.Modify(ctx, volId, rdPropsModify)
+ deleteProps = append(deleteProps,
+ linstor.PropertyDrbdNetProtocol,
+ linstor.PropertyCsiProtocolOverride,
+ )
+ }
+
+ err = s.client.ResourceDefinitions.Modify(ctx, volId, lapi.GenericPropsModify{
+ DeleteProps: deleteProps,
+ })
if err != nil {
return nil404(err)
}
@@ -805,6 +844,33 @@ func (s *Linstor) Detach(ctx context.Context, volId, node string) error {
return nil404(s.client.Resources.Delete(ctx, volId, node))
}
+// getEffectiveDrbdProtocol returns the DRBD network protocol ("A", "B" or "C")
+// that LINSTOR will hand to drbdadm for the given resource. Resource-definition
+// properties take precedence; otherwise the value is inherited from the
+// resource-group. An empty string is returned when neither level sets the
+// property (LINSTOR's compiled-in default is "C").
+func (s *Linstor) getEffectiveDrbdProtocol(ctx context.Context, volId string) (string, error) {
+ rd, err := s.client.ResourceDefinitions.Get(ctx, volId)
+ if err != nil {
+ return "", err
+ }
+
+ if v, ok := rd.Props[linstor.PropertyDrbdNetProtocol]; ok {
+ return v, nil
+ }
+
+ if rd.ResourceGroupName == "" {
+ return "", nil
+ }
+
+ rg, err := s.client.ResourceGroups.Get(ctx, rd.ResourceGroupName)
+ if err != nil {
+ return "", err
+ }
+
+ return rg.Props[linstor.PropertyDrbdNetProtocol], nil
+}
+
// CapacityBytes returns the amount of free space in the storage pool specified by the params and topology.
func (s *Linstor) CapacityBytes(ctx context.Context, storagePools []string, overProvision *float64, segments map[string]string) (int64, error) {
log := s.log.WithField("storage-pools", storagePools).WithField("segments", segments)
diff --git a/pkg/linstor/const.go b/pkg/linstor/const.go
index 9a5f79c..c8bc9c3 100644
--- a/pkg/linstor/const.go
+++ b/pkg/linstor/const.go
@@ -44,6 +44,19 @@ const (
// PropertyAllowTwoPrimaries is DRBD option to allow second primary. Mainly used for live-migration.
PropertyAllowTwoPrimaries = lc.NamespcDrbdNetOptions + "/allow-two-primaries"
+ // PropertyDrbdNetProtocol is the DRBD network replication protocol option
+ // (A = async, B = semi-sync, C = sync). DRBD requires Protocol C whenever
+ // allow-two-primaries is enabled, otherwise drbdadm adjust fails with
+ // "Protocol C required" (errno 139).
+ PropertyDrbdNetProtocol = lc.NamespcDrbdNetOptions + "/protocol"
+
+ // PropertyCsiProtocolOverride marks a resource-connection where this driver
+ // has installed a temporary Protocol=C override to make a Protocol-A/B
+ // resource compatible with allow-two-primaries during live migration. The
+ // marker lets us safely remove only the overrides we set, without touching
+ // connection-level overrides installed by the operator.
+ PropertyCsiProtocolOverride = lc.NamespcAuxiliary + "/csi-protocol-override"
+
// CreatedForTemporaryDisklessAttach marks a resource as temporary, i.e. it should be removed after it is no longer
// needed.
CreatedForTemporaryDisklessAttach = "temporary-diskless-attach"