Skip to content

feat(linstor): add linstor-csi patches for clone/snapshot relocation - #2133

Merged
Andrei Kvapil (kvaps) merged 7 commits into
mainfrom
feat/linstor-csi-relocate-after-clone-restore
Mar 16, 2026
Merged

Andrei Kvapil (kvaps) merged 7 commits into
mainfrom
feat/linstor-csi-relocate-after-clone-restore

Conversation

@kvaps

@kvaps Andrei Kvapil (kvaps) commented Mar 2, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Remove merged RWX validation patch (001-rwx-validation.diff)
  • Add new patch (001-relocate-after-clone-restore.diff) that includes:
    • Randomized node selection for snapshot restore
    • linstor.csi.linbit.com/relocateAfterClone StorageClass parameter to relocate replicas to optimal nodes after clone
    • snap.linstor.csi.linbit.com/relocate-after-restore VolumeSnapshotClass parameter to relocate replicas to optimal nodes after snapshot restore
  • Both parameters are disabled by default
  • Placing the snapshot restore parameter in VolumeSnapshotClass prevents unwanted relocation when Velero creates temporary PVCs during data mover backup

Upstream PRs:

Test plan

  • Clone a PVC and verify relocation logic executes
  • Restore a PVC from snapshot and verify replicas get migrated to optimal nodes
  • Verified on dev5 cluster (3-node) — snapshot restore triggered actual migration (node0 → node2)

Summary by CodeRabbit

Release Notes

  • New Features

    • Automatic resource relocation after clone and restore operations optimizes storage placement and load balancing across nodes.
    • RWX block attachment validation with optional disable flag ensures proper multi-pod access control.
  • Chores

    • Updated CDI clone strategy to use CSI-clone for improved efficiency.

@coderabbitai

coderabbitai Bot commented Mar 2, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This pull request modifies the CDI clone strategy configuration from "copy" to "csi-clone" and introduces two Linstor patches: one adds relocation logic after clone and restore operations with resource placement optimization, and another implements RWX block attachment validation with VM resolution capabilities.

Changes

Cohort / File(s) Summary
CDI Configuration
packages/system/kubevirt-cdi/templates/cdi-cr.yaml
Updated spec.cloneStrategyOverride from "copy" to "csi-clone" for datavolume cloning.
Linstor Relocation After Clone/Restore
packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff
Introduced relocateResources() function to trigger resource relocation after clone/restore if enabled; added RelocateAfterClone and RelocateAfterRestore parameters; updated resource queries and snapshot restoration logic to support relocation with idempotency guards and autoplacer-based optimization.
Linstor RWX Validation
packages/system/linstor/images/linstor-csi/patches/001-rwx-validation.diff
Added RWX block attachment validation module with DisableRWXBlockValidation flag; implemented ValidateRWXBlockAttachment() and GetVMNameFromPod() functions to validate multi-pod/VM scenarios; integrated validation into ControllerPublishVolume flow with comprehensive test coverage.

Sequence Diagram(s)

sequenceDiagram
    participant Clone as Clone/Restore Operation
    participant Linstor as Linstor Client
    participant Autoplacer as Autoplacer
    participant Resource as Resource Manager
    
    Clone->>Linstor: Trigger clone/restore with relocation flag
    Linstor->>Linstor: Check relocation parameter enabled
    rect rgba(100, 150, 200, 0.5)
        Linstor->>Autoplacer: Query optimal placement & storage info
        Autoplacer-->>Linstor: Placement recommendations per node
    end
    rect rgba(150, 100, 200, 0.5)
        Linstor->>Resource: Determine nodes to add/remove
        Linstor->>Resource: Migrate diskless resources
        Resource-->>Linstor: Migration complete
    end
    Linstor->>Linstor: Mark relocation as triggered
    Linstor-->>Clone: Relocation done (best-effort)
Loading
sequenceDiagram
    participant Controller as ControllerPublishVolume
    participant Validator as RWX Validator
    participant KubeAPI as Kubernetes API
    participant VMResolver as VM Name Resolver
    
    Controller->>Controller: Check if validation enabled
    alt Validation not disabled
        Controller->>Validator: ValidateRWXBlockAttachment(volumeID)
        rect rgba(100, 150, 200, 0.5)
            Validator->>KubeAPI: List pods with volume attachment
            KubeAPI-->>Validator: Pod list
        end
        rect rgba(150, 100, 200, 0.5)
            Validator->>VMResolver: GetVMNameFromPod(pod)
            KubeAPI->>VMResolver: Resolve owner references
            VMResolver-->>Validator: VM name or pod name
        end
        Validator-->>Controller: Validation result (pass/fail + owner name)
    end
    Controller-->>Controller: Proceed with publish
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A patch hops in with strategies new,
Clone via CSI, relocation too!
RWX validation guards the way,
Resources dance and find their play,
Storage springs forth, optimized and bright! 🌱

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding linstor-csi patches for clone and snapshot relocation functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/linstor-csi-relocate-after-clone-restore

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the LINSTOR CSI driver by streamlining its patch management and introducing significant improvements to volume lifecycle operations. It removes an obsolete patch, integrates new logic for intelligent replica relocation following volume cloning and snapshot restoration, and optimizes snapshot restore processes through randomized node selection. These changes aim to improve resource utilization and operational efficiency within LINSTOR-backed storage environments.

Highlights

  • Replica Relocation: Introduced automatic relocation of volume replicas to optimal nodes after clone and snapshot restore operations, improving data placement efficiency.
  • Snapshot Restore Optimization: Implemented randomized node selection for snapshot restores, distributing load and preventing concentration on single nodes.
  • StorageClass Parameters: Added new StorageClass parameters, relocateAfterClone and relocateAfterSnapshotRestore, which are enabled by default to control the new relocation behavior.
  • Patch Management: Removed an outdated RWX validation patch (001-rwx-validation.diff) as its functionality has been merged upstream, and added a new patch (001-relocate-after-clone-restore.diff) for the new features.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff
    • Added a new patch file to introduce replica relocation and randomized snapshot node selection features.
    • Implemented relocateResources function to asynchronously migrate replicas to optimal nodes based on LINSTOR's autoplacer.
    • Modified Clone and VolFromSnap methods to trigger replica relocation if enabled by StorageClass parameters.
    • Updated snapshot restore logic to select a random node from preferred or available candidates.
    • Introduced RelocateAfterClone and RelocateAfterSnapshotRestore parameters in pkg/volume/parameter.go, enabled by default.
  • packages/system/linstor/images/linstor-csi/patches/001-rwx-validation.diff
    • Removed an outdated patch file for RWX validation, as its functionality has been merged upstream into the LINSTOR CSI driver.
Activity
  • The pull request removes an outdated patch (001-rwx-validation.diff) because its functionality was merged upstream.
  • A new patch (001-relocate-after-clone-restore.diff) was added to introduce new features for clone/snapshot relocation.
  • The author has verified the relocation logic for PVC cloning and snapshot restoration on a 3-node dev5 cluster, observing actual replica migration.
  • The changes are based on upstream LINSTOR CSI pull requests: https://github.com/piraeusdatastore/linstor-csi/pull/418 and https://github.com/piraeusdatastore/linstor-csi/pull/419.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a feature to relocate volume replicas to optimal nodes after clone and snapshot restore operations. It also improves the node selection logic for snapshot restores by using randomization to distribute the load. The changes look good overall. I've added a couple of comments regarding potential improvements and compatibility.

+ }
+
+ // Only migrate min(remove, add) pairs.
+ pairs := min(len(nodesToRemove), len(nodesToAdd))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The min function, used here, was introduced as a built-in in Go 1.21. The use of the slices package elsewhere suggests the project might be on Go 1.18+, but not necessarily 1.21+. If the target Go version is below 1.21, this will cause a compilation failure.

To ensure backward compatibility, consider replacing it with a standard comparison. For example:

pairs := len(nodesToRemove)
if len(nodesToAdd) < pairs {
	pairs = len(nodesToAdd)
}

+
+ var selectedNode string
+ if len(candidates) > 0 {
+ selectedNode = candidates[rand.Intn(len(candidates))]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using math/rand.Intn without seeding results in a predictable sequence of numbers on each program start. This could cause restores to consistently pick the same "random" node after a pod restart, negating the benefit of randomization for load distribution.

For better randomness, consider using the crypto/rand package, which is more suitable for this purpose and doesn't require seeding. Alternatively, if you stick with math/rand, ensure it is seeded once at program startup (e.g., in main()).

@kvaps
Andrei Kvapil (kvaps) force-pushed the feat/linstor-csi-relocate-after-clone-restore branch from a46621d to bf70831 Compare March 2, 2026 17:43
@kvaps
Andrei Kvapil (kvaps) marked this pull request as ready for review March 2, 2026 17:55
@dosubot dosubot Bot added size/XXL This PR changes 1000+ lines, ignoring generated files kind/feature Categorizes issue or PR as related to a new feature labels Mar 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`:
- Around line 171-204: The relocation marker propertyRelocationTriggered is
being set unconditionally before any migrations; change this so the code sets
propertyRelocationTriggered only after a successful migration has been started.
Remove the initial s.client.ResourceDefinitions.Modify call and instead, inside
the loop over pairs (where s.client.Resources.MakeAvailable and
s.client.Resources.Migrate are called), after a successful Migrate (or
immediately after MakeAvailable if you consider that “started”), call
s.client.ResourceDefinitions.Modify to set
OverrideProps{propertyRelocationTriggered: "true"}; ensure you set it only once
(use a boolean flag like relocationMarked), and if the Modify call fails
log/warn but do not treat it as fatal so other pairs still process.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a46195 and c79926b.

📒 Files selected for processing (4)
  • packages/system/kubevirt-cdi/templates/cdi-cr.yaml
  • packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff
  • packages/system/linstor/images/linstor-csi/patches/001-rwx-validation.diff
  • packages/system/linstor/templates/volumesnapshotclass.yaml
💤 Files with no reviewable changes (1)
  • packages/system/linstor/images/linstor-csi/patches/001-rwx-validation.diff

Comment on lines +171 to +204
+ // Mark relocation as triggered before starting migrations.
+ err = s.client.ResourceDefinitions.Modify(ctx, volID, lapi.GenericPropsModify{
+ OverrideProps: map[string]string{propertyRelocationTriggered: "true"},
+ })
+ if err != nil {
+ return fmt.Errorf("failed to set relocation property: %w", err)
+ }
+
+ // Migrate each pair sequentially (LINSTOR serializes at the RD level anyway).
+ for i := range pairs {
+ fromNode := nodesToRemove[i]
+ toNode := nodesToAdd[i]
+ storPool := optimalPools[toNode]
+
+ logger := logger.WithFields(logrus.Fields{"from": fromNode, "to": toNode, "storagePool": storPool})
+
+ logger.Info("creating diskless resource on target node")
+
+ err := s.client.Resources.MakeAvailable(ctx, volID, toNode, lapi.ResourceMakeAvailable{})
+ if err != nil {
+ logger.WithError(err).Warn("failed to create diskless on target, skipping this pair")
+ continue
+ }
+
+ logger.Info("initiating migrate-disk")
+
+ err = s.client.Resources.Migrate(ctx, volID, fromNode, toNode, storPool)
+ if err != nil {
+ logger.WithError(err).Warn("migrate-disk failed, skipping this pair")
+ continue
+ }
+ }
+
+ return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Set the relocation marker after successful migration start, not before.
propertyRelocationTriggered is written before any migration succeeds. If all pairs fail transiently, later flows will skip relocation forever for that volume.

🔧 Proposed fix
-	// Mark relocation as triggered before starting migrations.
-	err = s.client.ResourceDefinitions.Modify(ctx, volID, lapi.GenericPropsModify{
-		OverrideProps: map[string]string{propertyRelocationTriggered: "true"},
-	})
-	if err != nil {
-		return fmt.Errorf("failed to set relocation property: %w", err)
-	}
-
-	// Migrate each pair sequentially (LINSTOR serializes at the RD level anyway).
+	// Migrate each pair sequentially (LINSTOR serializes at the RD level anyway).
+	migrated := false
 	for i := range pairs {
 		fromNode := nodesToRemove[i]
 		toNode := nodesToAdd[i]
 		storPool := optimalPools[toNode]
@@
 		err = s.client.Resources.Migrate(ctx, volID, fromNode, toNode, storPool)
 		if err != nil {
 			logger.WithError(err).Warn("migrate-disk failed, skipping this pair")
 			continue
 		}
+
+		migrated = true
 	}
+
+	if !migrated {
+		return fmt.Errorf("relocation could not be started on any migration pair")
+	}
+
+	err = s.client.ResourceDefinitions.Modify(ctx, volID, lapi.GenericPropsModify{
+		OverrideProps: map[string]string{propertyRelocationTriggered: "true"},
+	})
+	if err != nil {
+		return fmt.Errorf("failed to set relocation property: %w", err)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+ // Mark relocation as triggered before starting migrations.
+ err = s.client.ResourceDefinitions.Modify(ctx, volID, lapi.GenericPropsModify{
+ OverrideProps: map[string]string{propertyRelocationTriggered: "true"},
+ })
+ if err != nil {
+ return fmt.Errorf("failed to set relocation property: %w", err)
+ }
+
+ // Migrate each pair sequentially (LINSTOR serializes at the RD level anyway).
+ for i := range pairs {
+ fromNode := nodesToRemove[i]
+ toNode := nodesToAdd[i]
+ storPool := optimalPools[toNode]
+
+ logger := logger.WithFields(logrus.Fields{"from": fromNode, "to": toNode, "storagePool": storPool})
+
+ logger.Info("creating diskless resource on target node")
+
+ err := s.client.Resources.MakeAvailable(ctx, volID, toNode, lapi.ResourceMakeAvailable{})
+ if err != nil {
+ logger.WithError(err).Warn("failed to create diskless on target, skipping this pair")
+ continue
+ }
+
+ logger.Info("initiating migrate-disk")
+
+ err = s.client.Resources.Migrate(ctx, volID, fromNode, toNode, storPool)
+ if err != nil {
+ logger.WithError(err).Warn("migrate-disk failed, skipping this pair")
+ continue
+ }
+ }
+
+ return nil
// Migrate each pair sequentially (LINSTOR serializes at the RD level anyway).
migrated := false
for i := range pairs {
fromNode := nodesToRemove[i]
toNode := nodesToAdd[i]
storPool := optimalPools[toNode]
logger := logger.WithFields(logrus.Fields{"from": fromNode, "to": toNode, "storagePool": storPool})
logger.Info("creating diskless resource on target node")
err := s.client.Resources.MakeAvailable(ctx, volID, toNode, lapi.ResourceMakeAvailable{})
if err != nil {
logger.WithError(err).Warn("failed to create diskless on target, skipping this pair")
continue
}
logger.Info("initiating migrate-disk")
err = s.client.Resources.Migrate(ctx, volID, fromNode, toNode, storPool)
if err != nil {
logger.WithError(err).Warn("migrate-disk failed, skipping this pair")
continue
}
migrated = true
}
if !migrated {
return fmt.Errorf("relocation could not be started on any migration pair")
}
err = s.client.ResourceDefinitions.Modify(ctx, volID, lapi.GenericPropsModify{
OverrideProps: map[string]string{propertyRelocationTriggered: "true"},
})
if err != nil {
return fmt.Errorf("failed to set relocation property: %w", err)
}
return nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`
around lines 171 - 204, The relocation marker propertyRelocationTriggered is
being set unconditionally before any migrations; change this so the code sets
propertyRelocationTriggered only after a successful migration has been started.
Remove the initial s.client.ResourceDefinitions.Modify call and instead, inside
the loop over pairs (where s.client.Resources.MakeAvailable and
s.client.Resources.Migrate are called), after a successful Migrate (or
immediately after MakeAvailable if you consider that “started”), call
s.client.ResourceDefinitions.Modify to set
OverrideProps{propertyRelocationTriggered: "true"}; ensure you set it only once
(use a boolean flag like relocationMarked), and if the Modify call fails
log/warn but do not treat it as fatal so other pairs still process.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Mar 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff (1)

171-177: ⚠️ Potential issue | 🟠 Major

Move relocation marker write to after first successful migration start.

propertyRelocationTriggered is set before any Migrate succeeds. If all pairs fail transiently, retries are permanently skipped for this volume.

🔧 Proposed fix
-	// Mark relocation as triggered before starting migrations.
-	err = s.client.ResourceDefinitions.Modify(ctx, volID, lapi.GenericPropsModify{
-		OverrideProps: map[string]string{propertyRelocationTriggered: "true"},
-	})
-	if err != nil {
-		return fmt.Errorf("failed to set relocation property: %w", err)
-	}
-
-	// Migrate each pair sequentially (LINSTOR serializes at the RD level anyway).
-	for i := range pairs {
+	// Migrate each pair sequentially (LINSTOR serializes at the RD level anyway).
+	relocationMarked := false
+	migrated := false
+	for i := range pairs {
 		fromNode := nodesToRemove[i]
 		toNode := nodesToAdd[i]
 		storPool := optimalPools[toNode]
@@
 		err = s.client.Resources.Migrate(ctx, volID, fromNode, toNode, storPool)
 		if err != nil {
 			logger.WithError(err).Warn("migrate-disk failed, skipping this pair")
 			continue
 		}
+
+		migrated = true
+		if !relocationMarked {
+			if err := s.client.ResourceDefinitions.Modify(ctx, volID, lapi.GenericPropsModify{
+				OverrideProps: map[string]string{propertyRelocationTriggered: "true"},
+			}); err != nil {
+				logger.WithError(err).Warn("failed to set relocation marker")
+			} else {
+				relocationMarked = true
+			}
+		}
 	}
+
+	if !migrated {
+		return fmt.Errorf("relocation could not be started on any migration pair")
+	}

Also applies to: 179-202

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`
around lines 171 - 177, The relocation marker (propertyRelocationTriggered) is
being set before any Migrate call succeeds which causes retries to be skipped if
initial migrations fail; change the flow so that
s.client.ResourceDefinitions.Modify(...) that sets propertyRelocationTriggered =
"true" is performed only after the first successful call to Migrate for the
volume (use volID/ctx and detect success from Migrate return), not before
starting the migration attempts; ensure the same fix is applied to the other
similar block around lines 179-202 so the marker is only written once a
migration has actually started successfully.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`:
- Around line 349-355: The switch branch currently checks for the wrong key
string (case "/relocateAfterRestore") so the VolumeSnapshotClass parameter
"relocate-after-restore" is ignored; update the switch in the parser to match
the documented parameter name by changing the case to "relocate-after-restore"
so parsed value is converted to bool and stored into p.RelocateAfterRestore
(keep the strconv.ParseBool logic and error handling intact).

---

Duplicate comments:
In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`:
- Around line 171-177: The relocation marker (propertyRelocationTriggered) is
being set before any Migrate call succeeds which causes retries to be skipped if
initial migrations fail; change the flow so that
s.client.ResourceDefinitions.Modify(...) that sets propertyRelocationTriggered =
"true" is performed only after the first successful call to Migrate for the
volume (use volID/ctx and detect success from Migrate return), not before
starting the migration attempts; ensure the same fix is applied to the other
similar block around lines 179-202 so the marker is only written once a
migration has actually started successfully.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c79926b and f5cf5b1.

📒 Files selected for processing (1)
  • packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff

Comment on lines +349 to +355
+ case "/relocateAfterRestore":
+ b, err := strconv.ParseBool(v)
+ if err != nil {
+ return nil, err
+ }
+
+ p.RelocateAfterRestore = b

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Parse key does not match documented snapshot parameter name.

This branch handles "/relocateAfterRestore", but the documented VolumeSnapshotClass parameter is relocate-after-restore. With the current key, the intended parameter is ignored and relocation-after-restore never activates.

🔧 Proposed fix
-		case "/relocateAfterRestore":
+		case "/relocate-after-restore", "/relocateAfterRestore":
 			b, err := strconv.ParseBool(v)
 			if err != nil {
 				return nil, err
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+ case "/relocateAfterRestore":
+ b, err := strconv.ParseBool(v)
+ if err != nil {
+ return nil, err
+ }
+
+ p.RelocateAfterRestore = b
case "/relocate-after-restore", "/relocateAfterRestore":
b, err := strconv.ParseBool(v)
if err != nil {
return nil, err
}
p.RelocateAfterRestore = b
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`
around lines 349 - 355, The switch branch currently checks for the wrong key
string (case "/relocateAfterRestore") so the VolumeSnapshotClass parameter
"relocate-after-restore" is ignored; update the switch in the parser to match
the documented parameter name by changing the case to "relocate-after-restore"
so parsed value is converted to bool and stored into p.RelocateAfterRestore
(keep the strconv.ParseBool logic and error handling intact).

Andrei Kvapil (kvaps) and others added 6 commits March 10, 2026 12:42
Remove merged RWX validation patch and add new patch that includes:
- Randomized node selection for snapshot restore
- Relocate replicas to optimal nodes after clone and snapshot restore

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Andrei Kvapil <[email protected]>
Move snapshot restore relocation parameter from StorageClass to
VolumeSnapshotClass to avoid unwanted relocation during Velero
data mover backup. Change relocateAfterClone default to false.

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Andrei Kvapil <[email protected]>
Use CSI clone instead of host-assisted copy for CDI DataVolume
cloning. This leverages LINSTOR's native rd-clone mechanism which
is significantly faster than pod-based data copying, and works
together with the new relocateAfterClone parameter.

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Andrei Kvapil <[email protected]>
Enable relocate-after-restore on the default linstor-snapshots class
so that PVCs restored from snapshots get replicas relocated to optimal
nodes.

Add a separate linstor-snapshots-velero class with the Velero
annotation and without relocation, so Velero's temporary data mover
PVCs are not relocated unnecessarily.

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Andrei Kvapil <[email protected]>
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Andrei Kvapil <[email protected]>
@kvaps
Andrei Kvapil (kvaps) force-pushed the feat/linstor-csi-relocate-after-clone-restore branch from f5cf5b1 to 37a6816 Compare March 10, 2026 11:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff (1)

171-177: ⚠️ Potential issue | 🟠 Major

Don’t persist the relocation marker before any migration actually starts.

If every MakeAvailable/Migrate attempt later in the loop fails transiently, this still writes Aux/csi-relocation-triggered=true, and future clone/restore flows will skip relocation forever for that volume. Move the marker write until after the first successful migration kickoff.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`
around lines 171 - 177, The relocation marker is being written too early:
instead of setting propertyRelocationTriggered via
s.client.ResourceDefinitions.Modify(volID, ...) before any migrations, move that
Modify call so it runs only after the first successful migration kickoff (i.e.,
after the code path that successfully calls MakeAvailable/Migrate for the first
time completes). Locate the block that currently sets
propertyRelocationTriggered and remove it from the pre-migration area; insert
the Modify call immediately after the first successful migration start (the
success branch of the MakeAvailable/Migrate loop) so transient failures do not
permanently set Aux/csi-relocation-triggered=true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`:
- Around line 171-177: The relocation marker is being written too early: instead
of setting propertyRelocationTriggered via
s.client.ResourceDefinitions.Modify(volID, ...) before any migrations, move that
Modify call so it runs only after the first successful migration kickoff (i.e.,
after the code path that successfully calls MakeAvailable/Migrate for the first
time completes). Locate the block that currently sets
propertyRelocationTriggered and remove it from the pre-migration area; insert
the Modify call immediately after the first successful migration start (the
success branch of the MakeAvailable/Migrate loop) so transient failures do not
permanently set Aux/csi-relocation-triggered=true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f04ad94-b127-4ac3-a33b-bf0566710818

📥 Commits

Reviewing files that changed from the base of the PR and between f5cf5b1 and 37a6816.

📒 Files selected for processing (4)
  • packages/system/kubevirt-cdi/templates/cdi-cr.yaml
  • packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff
  • packages/system/linstor/images/linstor-csi/patches/001-rwx-validation.diff
  • packages/system/linstor/templates/volumesnapshotclass.yaml
💤 Files with no reviewable changes (1)
  • packages/system/linstor/images/linstor-csi/patches/001-rwx-validation.diff
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/system/linstor/templates/volumesnapshotclass.yaml

Hardcode relocateAfterClone=true and relocateAfterRestore=false as
defaults in the CSI driver patch instead of exposing them via
StorageClass/VolumeSnapshotClass parameters.

Remove the extra linstor-snapshots-ephemeral VolumeSnapshotClass (Velero)
and the relocateAfterRestore parameter from the default VolumeSnapshotClass.

This is a temporary measure while upstream linstor-server is deciding on
the interface for the rebalance feature (see LINBIT/linstor-server#487).
Once upstream provides native rebalance support, these hardcoded defaults
will be replaced by the proper upstream mechanism.

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Andrei Kvapil <[email protected]>
@kvaps

Copy link
Copy Markdown
Member Author

Updated the branch: hardcoded relocateAfterClone=true and relocateAfterRestore=false as defaults directly in the CSI driver patch. Removed the relocateAfterRestore parameter from VolumeSnapshotClass and removed the extra linstor-snapshots-ephemeral VolumeSnapshotClass for Velero.

This is a temporary measure while upstream linstor-server is deciding on the interface for native rebalance support (see LINBIT/linstor-server#487). Once the upstream mechanism is available, these hardcoded defaults will be replaced accordingly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff (2)

349-355: ⚠️ Potential issue | 🟠 Major

Parse key does not match the JSON field naming convention.

The switch case checks for /relocateAfterRestore (camelCase), but the JSON tag on the struct field is relocate-after-restore (kebab-case). This inconsistency may cause the parameter to be ignored when users specify it using the expected kebab-case format in VolumeSnapshotClass.

🔧 Proposed fix to accept both formats
-		case "/relocateAfterRestore":
+		case "/relocate-after-restore", "/relocateAfterRestore":
 			b, err := strconv.ParseBool(v)
 			if err != nil {
 				return nil, err
 			}
 
 			p.RelocateAfterRestore = b
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`
around lines 349 - 355, The switch branch currently matches
"/relocateAfterRestore" (camelCase) but the struct JSON tag is
"relocate-after-restore" (kebab-case), so update the parsing in the same switch
that sets p.RelocateAfterRestore to accept the kebab-case key
("/relocate-after-restore") — ideally accept both "/relocate-after-restore" and
"/relocateAfterRestore" by adding a case for the kebab form (or a second case
line) before parsing with strconv.ParseBool and assigning to
p.RelocateAfterRestore; ensure error handling remains the same.

171-204: ⚠️ Potential issue | 🟠 Major

Set the relocation marker after at least one successful migration, not before.

The propertyRelocationTriggered property is written before any migration attempt. If all migrations fail transiently, subsequent operations will skip relocation permanently for this volume since the marker is already set.

🔧 Proposed fix
-	// Mark relocation as triggered before starting migrations.
-	err = s.client.ResourceDefinitions.Modify(ctx, volID, lapi.GenericPropsModify{
-		OverrideProps: map[string]string{propertyRelocationTriggered: "true"},
-	})
-	if err != nil {
-		return fmt.Errorf("failed to set relocation property: %w", err)
-	}
-
 	// Migrate each pair sequentially (LINSTOR serializes at the RD level anyway).
+	migrated := false
 	for i := range pairs {
 		fromNode := nodesToRemove[i]
 		toNode := nodesToAdd[i]
 		storPool := optimalPools[toNode]
 
 		logger := logger.WithFields(logrus.Fields{"from": fromNode, "to": toNode, "storagePool": storPool})
 
 		logger.Info("creating diskless resource on target node")
 
 		err := s.client.Resources.MakeAvailable(ctx, volID, toNode, lapi.ResourceMakeAvailable{})
 		if err != nil {
 			logger.WithError(err).Warn("failed to create diskless on target, skipping this pair")
 			continue
 		}
 
 		logger.Info("initiating migrate-disk")
 
 		err = s.client.Resources.Migrate(ctx, volID, fromNode, toNode, storPool)
 		if err != nil {
 			logger.WithError(err).Warn("migrate-disk failed, skipping this pair")
 			continue
 		}
+
+		migrated = true
 	}
+
+	if migrated {
+		err = s.client.ResourceDefinitions.Modify(ctx, volID, lapi.GenericPropsModify{
+			OverrideProps: map[string]string{propertyRelocationTriggered: "true"},
+		})
+		if err != nil {
+			logger.WithError(err).Warn("failed to set relocation property")
+		}
+	}
 
 	return nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`
around lines 171 - 204, The relocation flag is being set too early; remove the
pre-loop call to s.client.ResourceDefinitions.Modify that writes
propertyRelocationTriggered and instead set propertyRelocationTriggered = "true"
only after at least one successful migrate (i.e., after a successful
s.client.Resources.Migrate call inside the for loop over pairs). Track a boolean
like migratedAny across the loop (use volID, nodesToRemove/nodesToAdd,
optimalPools and the existing logger), and if migratedAny is true call
s.client.ResourceDefinitions.Modify(ctx, volID,
lapi.GenericPropsModify{OverrideProps:
map[string]string{propertyRelocationTriggered: "true"}}) and handle/return any
error from that Modify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`:
- Around line 223-241: NodesAndConditionFromResources currently adds to allNodes
only when a resource has a volume with linstor.PropertyCreatedFor ==
linstor.CreatedForTemporaryDisklessAttach but still appends any res.NodeName to
abnormalNodes when res.State == nil, causing mismatch; update the abnormalNodes
append to use the same CSI-publish-target predicate (check res.Volumes with the
slices.ContainsFunc test against linstor.PropertyCreatedFor and
linstor.CreatedForTemporaryDisklessAttach) so you only record an abnormal node
if it is also included in allNodes, keeping both lists consistent.

---

Duplicate comments:
In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`:
- Around line 349-355: The switch branch currently matches
"/relocateAfterRestore" (camelCase) but the struct JSON tag is
"relocate-after-restore" (kebab-case), so update the parsing in the same switch
that sets p.RelocateAfterRestore to accept the kebab-case key
("/relocate-after-restore") — ideally accept both "/relocate-after-restore" and
"/relocateAfterRestore" by adding a case for the kebab form (or a second case
line) before parsing with strconv.ParseBool and assigning to
p.RelocateAfterRestore; ensure error handling remains the same.
- Around line 171-204: The relocation flag is being set too early; remove the
pre-loop call to s.client.ResourceDefinitions.Modify that writes
propertyRelocationTriggered and instead set propertyRelocationTriggered = "true"
only after at least one successful migrate (i.e., after a successful
s.client.Resources.Migrate call inside the for loop over pairs). Track a boolean
like migratedAny across the loop (use volID, nodesToRemove/nodesToAdd,
optimalPools and the existing logger), and if migratedAny is true call
s.client.ResourceDefinitions.Modify(ctx, volID,
lapi.GenericPropsModify{OverrideProps:
map[string]string{propertyRelocationTriggered: "true"}}) and handle/return any
error from that Modify.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2600c774-d966-447b-9b56-d837dbdb93a9

📥 Commits

Reviewing files that changed from the base of the PR and between 37a6816 and 450e2e8.

📒 Files selected for processing (1)
  • packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff

Comment on lines +223 to +241
-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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent filtering between allNodes and abnormalNodes.

The new logic only adds nodes to allNodes if they have the temporary-diskless-attach property, but abnormalNodes still includes any resource with State == nil, regardless of whether it's a CSI publish target. This could report abnormal conditions for nodes that aren't included in allNodes.

🔧 Proposed fix to make abnormalNodes consistent
 	for i := range ress {
 		res := &ress[i]
 
 		// 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 {
+		isCsiTarget := slices.ContainsFunc(res.Volumes, func(v lapi.Volume) bool {
 			createdFor, ok := v.Props[linstor.PropertyCreatedFor]
 			return ok && createdFor == linstor.CreatedForTemporaryDisklessAttach
-		}) {
+		})
+
+		if isCsiTarget {
 			allNodes = append(allNodes, res.NodeName)
-		}
 
-		if res.State == nil {
-			abnormalNodes = append(abnormalNodes, res.NodeName)
+			if res.State == nil {
+				abnormalNodes = append(abnormalNodes, res.NodeName)
+			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-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)
// A resource is a CSI publish target if any of its volumes were created
// by ControllerPublishVolume, identified by the temporary-diskless-attach property.
isCsiTarget := slices.ContainsFunc(res.Volumes, func(v lapi.Volume) bool {
createdFor, ok := v.Props[linstor.PropertyCreatedFor]
return ok && createdFor == linstor.CreatedForTemporaryDisklessAttach
})
if isCsiTarget {
allNodes = append(allNodes, res.NodeName)
if res.State == nil {
abnormalNodes = append(abnormalNodes, res.NodeName)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/system/linstor/images/linstor-csi/patches/001-relocate-after-clone-restore.diff`
around lines 223 - 241, NodesAndConditionFromResources currently adds to
allNodes only when a resource has a volume with linstor.PropertyCreatedFor ==
linstor.CreatedForTemporaryDisklessAttach but still appends any res.NodeName to
abnormalNodes when res.State == nil, causing mismatch; update the abnormalNodes
append to use the same CSI-publish-target predicate (check res.Volumes with the
slices.ContainsFunc test against linstor.PropertyCreatedFor and
linstor.CreatedForTemporaryDisklessAttach) so you only record an abnormal node
if it is also included in allNodes, keeping both lists consistent.

@kvaps
Andrei Kvapil (kvaps) merged commit 47dbbb9 into main Mar 16, 2026
10 of 11 checks passed
@kvaps
Andrei Kvapil (kvaps) deleted the feat/linstor-csi-relocate-after-clone-restore branch March 16, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Categorizes issue or PR as related to a new feature lgtm This PR has been approved by a maintainer size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants