Skip to content

Commit 03d4497

Browse files
committed
Implementation of namespaced-volume migration
1 parent 4af49b5 commit 03d4497

9 files changed

Lines changed: 252 additions & 19 deletions

File tree

‎atlas-lib/controlplane/volumes.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,15 @@ func (p CreateVolumeParams) toUnderscore(size int) cpapi.UnderscoreCreateParams
190190
if p.Namespaced {
191191
u.Namespaced = ptr.To(true)
192192
}
193+
// Always sent, unlike the other optional fields: the CSI driver sends an
194+
// explicit 1 for a StorageClass that omits max_namespace_per_subsys, so
195+
// omitting it here would make identical inputs depend on the control plane's
196+
// own default and could silently produce a shared subsystem.
197+
maxNS := 1
193198
if p.MaxNamespacePerSubsys > 0 {
194-
u.MaxNamespacePerSubsys = ptr.To(p.MaxNamespacePerSubsys)
199+
maxNS = p.MaxNamespacePerSubsys
195200
}
201+
u.MaxNamespacePerSubsys = ptr.To(maxNS)
196202
if p.NDCS > 0 {
197203
u.Ndcs = ptr.To(p.NDCS)
198204
}

‎helm-charts/charts/simplyblock-operator/crds/storage.simplyblock.io_volumemigrations.yaml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ spec:
146146
MigrationUUID is the identifier returned by the storage API when the
147147
migration was submitted. Used for polling and cancellation.
148148
type: string
149+
multiNamespace:
150+
description: |-
151+
MultiNamespace records that the volume shares its NVMe subsystem with
152+
sibling volumes ("namespaced" volumes), so moving it may affect more than
153+
one volume. Resolved once when the migration is submitted, from the
154+
StorageClass's max_namespace_per_subsys, and passed to the backend's
155+
ContinueMigration as the batch flag.
156+
type: boolean
149157
phase:
150158
description: Phase is the current lifecycle phase of the migration.
151159
enum:

‎operator/api/v1alpha1/volumemigration_types.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ type VolumeMigrationStatus struct {
103103
// Set during the Validating phase; cleared when the phase advances to Running.
104104
ValidationJobName string `json:"validationJobName,omitempty"`
105105

106+
// MultiNamespace records that the volume shares its NVMe subsystem with
107+
// sibling volumes ("namespaced" volumes), so moving it may affect more than
108+
// one volume. Resolved once when the migration is submitted, from the
109+
// StorageClass's max_namespace_per_subsys, and passed to the backend's
110+
// ContinueMigration as the batch flag.
111+
MultiNamespace bool `json:"multiNamespace,omitempty"`
112+
106113
// StartedAt is the time the migration was submitted to the storage API.
107114
StartedAt *metav1.Time `json:"startedAt,omitempty"`
108115

‎operator/config/crd/bases/storage.simplyblock.io_volumemigrations.yaml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ spec:
146146
MigrationUUID is the identifier returned by the storage API when the
147147
migration was submitted. Used for polling and cancellation.
148148
type: string
149+
multiNamespace:
150+
description: |-
151+
MultiNamespace records that the volume shares its NVMe subsystem with
152+
sibling volumes ("namespaced" volumes), so moving it may affect more than
153+
one volume. Resolved once when the migration is submitted, from the
154+
StorageClass's max_namespace_per_subsys, and passed to the backend's
155+
ContinueMigration as the batch flag.
156+
type: boolean
149157
phase:
150158
description: Phase is the current lifecycle phase of the migration.
151159
enum:

‎operator/dist/install.yaml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3579,6 +3579,14 @@ spec:
35793579
MigrationUUID is the identifier returned by the storage API when the
35803580
migration was submitted. Used for polling and cancellation.
35813581
type: string
3582+
multiNamespace:
3583+
description: |-
3584+
MultiNamespace records that the volume shares its NVMe subsystem with
3585+
sibling volumes ("namespaced" volumes), so moving it may affect more than
3586+
one volume. Resolved once when the migration is submitted, from the
3587+
StorageClass's max_namespace_per_subsys, and passed to the backend's
3588+
ContinueMigration as the batch flag.
3589+
type: boolean
35823590
phase:
35833591
description: Phase is the current lifecycle phase of the migration.
35843592
enum:

‎operator/internal/autoplacement/logical_volume_selector.go‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,6 @@ func (lvs *LogicalVolumeSelector) BuildCSIManagedVolumes(ctx context.Context, cl
325325
return managed, nil
326326
}
327327

328-
// storageClassResolver adapts the controller-runtime client to
329-
// kube.StorageClassResolver, so the rebalancer resolves provisioning Properties
330-
// through the shared kube package rather than reimplementing the parsing. Gets
331328
// BuildNamespacedSet returns the set of simplyblock CSI-managed volume UUIDs in
332329
// the cluster whose StorageClass provisions a multi-namespace NVMe subsystem
333330
// (max_namespace_per_subsys > 1). Such volumes share a subsystem with sibling

‎operator/internal/controller/volumemigration_controller.go‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
atlaskube "github.com/simplyblock/atlas/kube"
1314
"github.com/simplyblock/atlas/ptr"
1415
vmigration "github.com/simplyblock/simplyblock-operator/internal/volumemigration"
1516
batchv1 "k8s.io/api/batch/v1"
@@ -37,6 +38,7 @@ import (
3738
// +kubebuilder:rbac:groups="",resources=pods/log,verbs=get
3839
// +kubebuilder:rbac:groups=storage.simplyblock.io,resources=storageclusters,verbs=get;list;watch
3940
// +kubebuilder:rbac:groups=storage.simplyblock.io,resources=storageclusters/status,verbs=get;update;patch
41+
// +kubebuilder:rbac:groups=storage.k8s.io,resources=storageclasses,verbs=get
4042

4143
// VolumeMigrationReconciler reconciles VolumeMigration resources.
4244
type VolumeMigrationReconciler struct {
@@ -50,6 +52,9 @@ type VolumeMigrationReconciler struct {
5052
// otherwise miss a genuinely-running consumer and cause validation to be
5153
// skipped for a live volume, breaking its I/O path after cutover.
5254
apiReader client.Reader
55+
// scResolver reads the StorageClass that provisioned a PV, to tell whether the
56+
// volume is namespaced (shares its NVMe subsystem with siblings).
57+
scResolver atlaskube.Resolver
5358
}
5459

5560
// errConsumerNotReady indicates that a pod references the volume's PVC but is not
@@ -161,6 +166,9 @@ func (r *VolumeMigrationReconciler) reconcileStart(
161166
vm.Status.ClusterUUID = clusterUUID
162167
vm.Status.VolumeUUID = volumeUUID
163168
vm.Status.PoolUUID = poolUUID
169+
// Resolved here, where the PV (and through it the StorageClass) is at hand, and
170+
// persisted so the later continue step needs no second lookup.
171+
vm.Status.MultiNamespace = r.isMultiNamespaceMigration(ctx, pv)
164172
// SourceNodeUUID and SnapsTotal are populated from GetMigration once status=Running.
165173
vm.Status.Connections = conns
166174
vm.Status.StartedAt = &now
@@ -174,6 +182,39 @@ func (r *VolumeMigrationReconciler) reconcileStart(
174182
return ctrl.Result{Requeue: true}, nil
175183
}
176184

185+
// isMultiNamespaceMigration reports whether the migration may affect more than
186+
// one volume, i.e. whether the volume's NVMe subsystem is shared with sibling
187+
// volumes ("namespaced" volumes). The answer comes from the StorageClass that
188+
// provisioned the PV: max_namespace_per_subsys > 1
189+
// (atlaskube.Properties.IsMultiNamespace) makes every volume of the class
190+
// shared, including one that is currently still the first — and so far only —
191+
// namespace in its subsystem. The volume's live NSID would be the host-side
192+
// counterpart, but it is not part of the control plane's volume or migration
193+
// data and the operator has no host sysfs access to Identify it.
194+
//
195+
// A StorageClass that cannot be resolved is treated as single-namespace and
196+
// logged, the same fail-open stance the rebalancer's namespaced-set collection
197+
// takes: one unreadable class must not stall a migration.
198+
func (r *VolumeMigrationReconciler) isMultiNamespaceMigration(
199+
ctx context.Context,
200+
pv *corev1.PersistentVolume,
201+
) bool {
202+
log := logf.FromContext(ctx)
203+
204+
props, err := atlaskube.ResolvePropertiesForPV(ctx, r.scResolver, pv)
205+
if err != nil {
206+
log.Info("Cannot resolve StorageClass for volume; assuming a single-namespace migration",
207+
"pv", pv.Name, "error", err.Error())
208+
return false
209+
}
210+
if props.IsMultiNamespace() {
211+
log.Info("StorageClass provisions namespaced volumes; migration is namespaced",
212+
"pv", pv.Name, "maxNamespacePerSubsys", props.MaxNamespacePerSubsys)
213+
return true
214+
}
215+
return false
216+
}
217+
177218
// reconcileValidating creates a Job on the target worker node that:
178219
// 1. Runs `nvme connect` for each connection returned by CreateMigration.
179220
// 2. Runs `nvme list --verbose` and verifies all new NQNs appear with ANA
@@ -336,7 +377,7 @@ func (r *VolumeMigrationReconciler) performMigration(
336377
log.Info("Migration already terminal before continue; advancing to Running for classification",
337378
"migration", vm.Status.MigrationUUID, "status", m.Status)
338379
case m.Phase == webapi.MigrationPhasePreCreated:
339-
if err := r.apiClient.ContinueMigration(ctx, vm.Status.ClusterUUID, vm.Status.PoolUUID, vm.Status.VolumeUUID, vm.Status.MigrationUUID); err != nil {
380+
if err := r.apiClient.ContinueMigration(ctx, vm.Status.ClusterUUID, vm.Status.PoolUUID, vm.Status.VolumeUUID, vm.Status.MigrationUUID, vm.Status.MultiNamespace); err != nil {
340381
// The continue may have taken effect despite the error. Only a
341382
// migration still stuck in pre_created is a genuine start failure
342383
// worth cancelling; anything else means it already advanced.
@@ -763,6 +804,7 @@ func (r *VolumeMigrationReconciler) SetupWithManager(
763804
return fmt.Errorf("create k8s client for log collection: %w", err)
764805
}
765806
r.coreClient = k8s.CoreV1()
807+
r.scResolver = atlaskube.NewLiveResolver(k8s)
766808
// Uncached reader for the consumer-detection decision (see resolveConsumerNodeName).
767809
r.apiReader = mgr.GetAPIReader()
768810
return ctrl.NewControllerManagedBy(mgr).

0 commit comments

Comments
 (0)