feat(linstor): bump linstor-csi to v1.10.6 with Protocol-C dual-attach fix (#2496)
## What this PR does Bumps `linstor-csi` from v1.10.5 to v1.10.6 and ships an out-of-tree patch that fixes live migration of KubeVirt VMs whose volumes sit on a DRBD Protocol-A/B resource (e.g. a `replicated-async` StorageClass). ### Problem DRBD requires Protocol C whenever `allow-two-primaries=yes` is enabled. Operators commonly opt into Protocol A on a per-resource-group basis for async / WAN replication, which silently breaks every subsequent live migration of consumers of those volumes: `drbdadm adjust` rejects the second-attach with `Protocol C required` (errno 139), KubeVirt's evacuation loop retries indefinitely, and the VM stays pinned to the source node. ### Change - `LINSTOR_CSI_VERSION` 1.10.5 → 1.10.6 (Makefile + Dockerfile default). - New patch `002-protocol-c-override-for-dual-attach.diff`: when `Attach` installs `allow-two-primaries=yes` on the resource-definition during a second attach, it also installs `DrbdOptions/Net/protocol=C` as an override on the resource-definition. The override applies to every connection (including diskless TieBreaker peers, where a per-pair override would still leave one connection broken). It is tagged with `Aux/csi-protocol-override=yes` so `Detach` removes only the override this driver installed, leaving any operator-set `Protocol` property on the resource-definition untouched. - Existing patch `001-relocate-after-clone-restore.diff` regenerated against v1.10.6 (context shift only, no logic change — the old patch hunks no longer aligned cleanly). ### Verification - `make image-linstor-csi` builds successfully on linux/amd64 with both patches applied. - End-to-end test on dev5 cluster (KubeVirt v1.6.3, 3-node Talos): created a Protocol-A resource-group + StorageClass, provisioned a VM on top, and triggered live migration. Migration succeeds in a single Attach with the override installed during dual-attach and removed by Detach. Reproducer (without the patch) is the well-known evacuation loop with `(node) Failed to adjust DRBD resource ... Protocol C required`. ### Upstream Upstreamed as draft PR piraeusdatastore/linstor-csi#435. ### Compatibility - No behaviour change for resources already using Protocol C (the common case). - No behaviour change for resources never attached with allow-two-primaries. - Idempotent: re-running `Attach` is a no-op once the override is installed. - Operator-set Protocol overrides on the resource-definition are preserved (gated by the Aux marker). ### Release note ```release-note fix(linstor): live migration of KubeVirt VMs on Protocol-A/B (async) DRBD volumes no longer fails with "Protocol C required" — linstor-csi now installs a Protocol=C override on the resource-definition during dual-attach and reverts it on detach. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Automatic replica relocation after volume clone and snapshot restore to improve placement and load distribution. * Conditional DRBD protocol override to enable/clean up dual-attach (two-primaries) scenarios more reliably. * **Chores** * Updated LINSTOR CSI default to v1.10.6. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
commit
f1e56417a0
4 changed files with 140 additions and 58 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue