[linstor] Refactor node-level RWX validation
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 3c4f0cd952)
This commit is contained in:
parent
c5222aae97
commit
e73bf9905d
1 changed files with 253 additions and 133 deletions
|
|
@ -1,100 +1,202 @@
|
|||
diff --git a/cmd/linstor-csi/linstor-csi.go b/cmd/linstor-csi/linstor-csi.go
|
||||
index 143f6cee..bd28e06e 100644
|
||||
--- a/cmd/linstor-csi/linstor-csi.go
|
||||
+++ b/cmd/linstor-csi/linstor-csi.go
|
||||
@@ -41,22 +41,23 @@ import (
|
||||
|
||||
func main() {
|
||||
var (
|
||||
- lsEndpoint = flag.String("linstor-endpoint", "", "Controller API endpoint for LINSTOR")
|
||||
- lsSkipTLSVerification = flag.Bool("linstor-skip-tls-verification", false, "If true, do not verify tls")
|
||||
- csiEndpoint = flag.String("csi-endpoint", "unix:///var/lib/kubelet/plugins/linstor.csi.linbit.com/csi.sock", "CSI endpoint")
|
||||
- node = flag.String("node", "", "Node ID to pass to node service")
|
||||
- logLevel = flag.String("log-level", "info", "Enable debug log output. Choose from: panic, fatal, error, warn, info, debug")
|
||||
- rps = flag.Float64("linstor-api-requests-per-second", 0, "Maximum allowed number of LINSTOR API requests per second. Default: Unlimited")
|
||||
- burst = flag.Int("linstor-api-burst", 1, "Maximum number of API requests allowed before being limited by requests-per-second. Default: 1 (no bursting)")
|
||||
- bearerTokenFile = flag.String("bearer-token", "", "Read the bearer token from the given file and use it for authentication.")
|
||||
- propNs = flag.String("property-namespace", linstor.NamespcAuxiliary, "Limit the reported topology keys to properties from the given namespace.")
|
||||
- labelBySP = flag.Bool("label-by-storage-pool", true, "Set to false to disable labeling of nodes based on their configured storage pools.")
|
||||
- nodeCacheTimeout = flag.Duration("node-cache-timeout", 1*time.Minute, "Duration for which the results of node and storage pool related API responses should be cached.")
|
||||
- resourceCacheTimeout = flag.Duration("resource-cache-timeout", 30*time.Second, "Duration for which the results of resource related API responses should be cached.")
|
||||
- resyncAfter = flag.Duration("resync-after", 5*time.Minute, "Duration after which reconciliations (such as for VolumeSnapshotClasses) should be rerun. Set to 0 to disable.")
|
||||
- enableRWX = flag.Bool("enable-rwx", false, "Enable RWX support via NFS (requires running in Kubernetes).")
|
||||
- namespace = flag.String("nfs-service-namespace", "", "The namespace the NFS service is running in.")
|
||||
- reactorConfigMapName = flag.String("nfs-reactor-config-map-name", "linstor-csi-nfs-reactor-config", "Name of the config map used to store promoter configuration")
|
||||
+ lsEndpoint = flag.String("linstor-endpoint", "", "Controller API endpoint for LINSTOR")
|
||||
+ lsSkipTLSVerification = flag.Bool("linstor-skip-tls-verification", false, "If true, do not verify tls")
|
||||
+ csiEndpoint = flag.String("csi-endpoint", "unix:///var/lib/kubelet/plugins/linstor.csi.linbit.com/csi.sock", "CSI endpoint")
|
||||
+ node = flag.String("node", "", "Node ID to pass to node service")
|
||||
+ logLevel = flag.String("log-level", "info", "Enable debug log output. Choose from: panic, fatal, error, warn, info, debug")
|
||||
+ rps = flag.Float64("linstor-api-requests-per-second", 0, "Maximum allowed number of LINSTOR API requests per second. Default: Unlimited")
|
||||
+ burst = flag.Int("linstor-api-burst", 1, "Maximum number of API requests allowed before being limited by requests-per-second. Default: 1 (no bursting)")
|
||||
+ bearerTokenFile = flag.String("bearer-token", "", "Read the bearer token from the given file and use it for authentication.")
|
||||
+ propNs = flag.String("property-namespace", linstor.NamespcAuxiliary, "Limit the reported topology keys to properties from the given namespace.")
|
||||
+ labelBySP = flag.Bool("label-by-storage-pool", true, "Set to false to disable labeling of nodes based on their configured storage pools.")
|
||||
+ nodeCacheTimeout = flag.Duration("node-cache-timeout", 1*time.Minute, "Duration for which the results of node and storage pool related API responses should be cached.")
|
||||
+ resourceCacheTimeout = flag.Duration("resource-cache-timeout", 30*time.Second, "Duration for which the results of resource related API responses should be cached.")
|
||||
+ resyncAfter = flag.Duration("resync-after", 5*time.Minute, "Duration after which reconciliations (such as for VolumeSnapshotClasses) should be rerun. Set to 0 to disable.")
|
||||
+ enableRWX = flag.Bool("enable-rwx", false, "Enable RWX support via NFS (requires running in Kubernetes).")
|
||||
+ namespace = flag.String("nfs-service-namespace", "", "The namespace the NFS service is running in.")
|
||||
+ reactorConfigMapName = flag.String("nfs-reactor-config-map-name", "linstor-csi-nfs-reactor-config", "Name of the config map used to store promoter configuration")
|
||||
+ disableRWXBlockValidation = flag.Bool("disable-rwx-block-validation", false, "Disable KubeVirt VM ownership validation for RWX block volumes.")
|
||||
)
|
||||
|
||||
flag.Var(&volume.DefaultRemoteAccessPolicy, "default-remote-access-policy", "")
|
||||
@@ -169,6 +170,10 @@ func main() {
|
||||
opts = append(opts, driver.ConfigureRWX(*namespace, *reactorConfigMapName))
|
||||
}
|
||||
|
||||
+ if *disableRWXBlockValidation {
|
||||
+ opts = append(opts, driver.DisableRWXBlockValidation())
|
||||
+ }
|
||||
+
|
||||
drv, err := driver.NewDriver(opts...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go
|
||||
index bea69a8..69e71a6 100644
|
||||
index bea69a8b..a39674b6 100644
|
||||
--- a/pkg/driver/driver.go
|
||||
+++ b/pkg/driver/driver.go
|
||||
@@ -707,6 +707,219 @@ func (d Driver) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest)
|
||||
return &csi.DeleteVolumeResponse{}, nil
|
||||
@@ -83,6 +83,8 @@ type Driver struct {
|
||||
topologyPrefix string
|
||||
// resyncAfter is the interval after which reconciliations should be retried
|
||||
resyncAfter time.Duration
|
||||
+ // disableRWXBlockValidation disables KubeVirt VM ownership validation for RWX block volumes
|
||||
+ disableRWXBlockValidation bool
|
||||
|
||||
// Embed for forward compatibility.
|
||||
csi.UnimplementedIdentityServer
|
||||
@@ -300,6 +302,17 @@ func ResyncAfter(resyncAfter time.Duration) func(*Driver) error {
|
||||
}
|
||||
}
|
||||
|
||||
+// DisableRWXBlockValidation disables the KubeVirt VM ownership validation for RWX block volumes.
|
||||
+// When disabled, the driver will not check if multiple pods using the same RWX block volume
|
||||
+// belong to the same VM. This may be needed in environments where the validation causes issues
|
||||
+// or when using RWX block volumes outside of KubeVirt.
|
||||
+func DisableRWXBlockValidation() func(*Driver) error {
|
||||
+ return func(d *Driver) error {
|
||||
+ d.disableRWXBlockValidation = true
|
||||
+ return nil
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
// GetPluginInfo https://github.com/container-storage-interface/spec/blob/v1.9.0/spec.md#getplugininfo
|
||||
func (d Driver) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
|
||||
return &csi.GetPluginInfoResponse{
|
||||
@@ -751,6 +764,14 @@ func (d Driver) ControllerPublishVolume(ctx context.Context, req *csi.Controller
|
||||
// ReadWriteMany block volume
|
||||
rwxBlock := req.VolumeCapability.AccessMode.GetMode() == csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER && req.VolumeCapability.GetBlock() != nil
|
||||
|
||||
+ // Validate RWX block attachment to prevent misuse of allow-two-primaries
|
||||
+ if rwxBlock && !d.disableRWXBlockValidation {
|
||||
+ if _, err := utils.ValidateRWXBlockAttachment(ctx, d.kubeClient, d.log, req.GetVolumeId()); err != nil {
|
||||
+ return nil, status.Errorf(codes.FailedPrecondition,
|
||||
+ "ControllerPublishVolume failed for %s: %v", req.GetVolumeId(), err)
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
devPath, err := d.Assignments.Attach(ctx, req.GetVolumeId(), req.GetNodeId(), rwxBlock)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal,
|
||||
diff --git a/pkg/utils/rwx_validation.go b/pkg/utils/rwx_validation.go
|
||||
new file mode 100644
|
||||
index 00000000..9fe82768
|
||||
--- /dev/null
|
||||
+++ b/pkg/utils/rwx_validation.go
|
||||
@@ -0,0 +1,263 @@
|
||||
+/*
|
||||
+CSI Driver for Linstor
|
||||
+Copyright © 2018 LINBIT USA, LLC
|
||||
+
|
||||
+This program is free software; you can redistribute it and/or modify
|
||||
+it under the terms of the GNU General Public License as published by
|
||||
+the Free Software Foundation; either version 2 of the License, or
|
||||
+(at your option) any later version.
|
||||
+
|
||||
+This program is distributed in the hope that it will be useful,
|
||||
+but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
+GNU General Public License for more details.
|
||||
+
|
||||
+You should have received a copy of the GNU General Public License
|
||||
+along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
+*/
|
||||
+
|
||||
+package utils
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "fmt"
|
||||
+
|
||||
+ "github.com/sirupsen/logrus"
|
||||
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
+ "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
+ "k8s.io/client-go/dynamic"
|
||||
+)
|
||||
+
|
||||
+// KubeVirtVMLabel is the label that KubeVirt adds to pods to identify the VM they belong to.
|
||||
+const KubeVirtVMLabel = "vm.kubevirt.io/name"
|
||||
+
|
||||
+// KubeVirtHotplugDiskLabel is the label that KubeVirt adds to hotplug disk pods.
|
||||
+const KubeVirtHotplugDiskLabel = "kubevirt.io"
|
||||
+
|
||||
+// podGVR is the GroupVersionResource for pods.
|
||||
+var podGVR = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
|
||||
+// PodGVR is the GroupVersionResource for pods.
|
||||
+var PodGVR = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
|
||||
+
|
||||
+// pvGVR is the GroupVersionResource for persistent volumes.
|
||||
+var pvGVR = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumes"}
|
||||
+// PVGVR is the GroupVersionResource for persistent volumes.
|
||||
+var PVGVR = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumes"}
|
||||
+
|
||||
+// getVMNameFromPod extracts the VM name from a pod, handling both regular virt-launcher pods
|
||||
+// and hotplug disk pods (which reference the virt-launcher pod via ownerReferences).
|
||||
+func (d Driver) getVMNameFromPod(ctx context.Context, pod *unstructured.Unstructured) (string, error) {
|
||||
+ labels := pod.GetLabels()
|
||||
+ if labels == nil {
|
||||
+ return "", nil
|
||||
+ }
|
||||
+
|
||||
+ // Direct case: pod has vm.kubevirt.io/name label (virt-launcher pod)
|
||||
+ if vmName, ok := labels[KubeVirtVMLabel]; ok && vmName != "" {
|
||||
+ return vmName, nil
|
||||
+ }
|
||||
+
|
||||
+ // Hotplug disk case: pod has kubevirt.io: hotplug-disk label
|
||||
+ // Follow ownerReferences to find the virt-launcher pod
|
||||
+ if hotplugValue, ok := labels[KubeVirtHotplugDiskLabel]; ok && hotplugValue == "hotplug-disk" {
|
||||
+ ownerRefs := pod.GetOwnerReferences()
|
||||
+ for _, owner := range ownerRefs {
|
||||
+ if owner.Kind != "Pod" || owner.Controller == nil || !*owner.Controller {
|
||||
+ continue
|
||||
+ }
|
||||
+
|
||||
+ // Get the owner pod (virt-launcher)
|
||||
+ ownerPod, err := d.kubeClient.Resource(podGVR).Namespace(pod.GetNamespace()).Get(ctx, owner.Name, metav1.GetOptions{})
|
||||
+ if err != nil {
|
||||
+ return "", fmt.Errorf("failed to get owner pod %s: %w", owner.Name, err)
|
||||
+ }
|
||||
+
|
||||
+ // Extract VM name from owner pod
|
||||
+ ownerLabels := ownerPod.GetLabels()
|
||||
+ if ownerLabels != nil {
|
||||
+ if vmName, ok := ownerLabels[KubeVirtVMLabel]; ok && vmName != "" {
|
||||
+ d.log.WithFields(logrus.Fields{
|
||||
+ "hotplugPod": pod.GetName(),
|
||||
+ "virtLauncher": owner.Name,
|
||||
+ "vmName": vmName,
|
||||
+ }).Debug("resolved VM name from hotplug disk pod via owner reference")
|
||||
+
|
||||
+ return vmName, nil
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return "", fmt.Errorf("owner pod %s does not have %s label", owner.Name, KubeVirtVMLabel)
|
||||
+ }
|
||||
+
|
||||
+ return "", fmt.Errorf("hotplug disk pod %s has no controller owner reference", pod.GetName())
|
||||
+ }
|
||||
+
|
||||
+ return "", nil
|
||||
+}
|
||||
+
|
||||
+// validateRWXBlockAttachment checks that RWX block volumes are only used by pods belonging to the same VM.
|
||||
+// ValidateRWXBlockAttachment checks that RWX block volumes are only used by pods belonging to the same VM.
|
||||
+// This prevents misuse of allow-two-primaries while still permitting live migration.
|
||||
+// Returns the VM name if validation passes, or an error if:
|
||||
+// - Multiple pods from different VMs are trying to use the same volume
|
||||
+// - A pod without the KubeVirt VM label is trying to use a volume already attached elsewhere (strict mode)
|
||||
+// Returns empty string for VM name when no pods are using the volume or validation is skipped.
|
||||
+func (d Driver) validateRWXBlockAttachment(ctx context.Context, volumeID string) (string, error) {
|
||||
+ d.log.WithField("volumeID", volumeID).Info("validateRWXBlockAttachment called")
|
||||
+func ValidateRWXBlockAttachment(ctx context.Context, kubeClient dynamic.Interface, log *logrus.Entry, volumeID string) (string, error) {
|
||||
+ log.WithField("volumeID", volumeID).Info("validateRWXBlockAttachment called")
|
||||
+
|
||||
+ if d.kubeClient == nil {
|
||||
+ if kubeClient == nil {
|
||||
+ // Not running in Kubernetes, skip validation
|
||||
+ d.log.Warn("validateRWXBlockAttachment: kubeClient is nil, skipping validation")
|
||||
+ log.Warn("validateRWXBlockAttachment: kubeClient is nil, skipping validation")
|
||||
+ return "", nil
|
||||
+ }
|
||||
+
|
||||
+ // Get PV to find PVC reference (volumeID == PV name in CSI)
|
||||
+ pv, err := d.kubeClient.Resource(pvGVR).Get(ctx, volumeID, metav1.GetOptions{})
|
||||
+ // Get PV to find PVC reference
|
||||
+ pv, err := kubeClient.Resource(PVGVR).Get(ctx, volumeID, metav1.GetOptions{})
|
||||
+ if err != nil {
|
||||
+ d.log.WithError(err).Warn("cannot validate RWX attachment: failed to get PV")
|
||||
+ log.WithError(err).Warn("cannot validate RWX attachment: failed to get PV")
|
||||
+ return "", nil
|
||||
+ }
|
||||
+
|
||||
+ // Verify that PV's volumeHandle matches the volumeID
|
||||
+ volumeHandle, found, err := unstructured.NestedString(pv.Object, "spec", "csi", "volumeHandle")
|
||||
+ if err != nil {
|
||||
+ log.WithError(err).Warnf("cannot validate RWX attachment: failed to read volumeHandle for PV %s", volumeID)
|
||||
+
|
||||
+ return "", nil
|
||||
+ }
|
||||
+
|
||||
+ if !found {
|
||||
+ log.Warnf("cannot validate RWX attachment: volumeHandle not found for PV %s", volumeID)
|
||||
+
|
||||
+ return "", nil
|
||||
+ }
|
||||
+
|
||||
+ if volumeHandle != volumeID {
|
||||
+ log.WithFields(logrus.Fields{
|
||||
+ "volumeID": volumeID,
|
||||
+ "volumeHandle": volumeHandle,
|
||||
+ }).Warn("cannot validate RWX attachment: PV volumeHandle does not match volumeID")
|
||||
+
|
||||
+ return "", nil
|
||||
+ }
|
||||
+
|
||||
+ // Extract claimRef from PV
|
||||
+ claimRef, found, _ := unstructured.NestedMap(pv.Object, "spec", "claimRef")
|
||||
+ if !found {
|
||||
+ d.log.Warn("cannot validate RWX attachment: PV has no claimRef")
|
||||
+ log.Warn("cannot validate RWX attachment: PV has no claimRef")
|
||||
+ return "", nil
|
||||
+ }
|
||||
+
|
||||
|
|
@ -102,12 +204,12 @@ index bea69a8..69e71a6 100644
|
|||
+ pvcNamespace, _, _ := unstructured.NestedString(claimRef, "namespace")
|
||||
+
|
||||
+ if pvcNamespace == "" || pvcName == "" {
|
||||
+ d.log.Warn("cannot validate RWX attachment: PVC name or namespace is empty in claimRef")
|
||||
+ log.Warn("cannot validate RWX attachment: PVC name or namespace is empty in claimRef")
|
||||
+ return "", nil
|
||||
+ }
|
||||
+
|
||||
+ // List all pods in the namespace
|
||||
+ podList, err := d.kubeClient.Resource(podGVR).Namespace(pvcNamespace).List(ctx, metav1.ListOptions{})
|
||||
+ podList, err := kubeClient.Resource(PodGVR).Namespace(pvcNamespace).List(ctx, metav1.ListOptions{})
|
||||
+ if err != nil {
|
||||
+ return "", fmt.Errorf("failed to list pods in namespace %s: %w", pvcNamespace, err)
|
||||
+ }
|
||||
|
|
@ -139,28 +241,25 @@ index bea69a8..69e71a6 100644
|
|||
+ continue
|
||||
+ }
|
||||
+
|
||||
+ pvc, found, _ := unstructured.NestedMap(volMap, "persistentVolumeClaim")
|
||||
+ if !found {
|
||||
+ claimName, found, _ := unstructured.NestedString(volMap, "persistentVolumeClaim", "claimName")
|
||||
+ if !found || claimName != pvcName {
|
||||
+ continue
|
||||
+ }
|
||||
+
|
||||
+ claimName, _, _ := unstructured.NestedString(pvc, "claimName")
|
||||
+ if claimName == pvcName {
|
||||
+ // Extract VM name, handling both regular and hotplug disk pods
|
||||
+ vmName, err := d.getVMNameFromPod(ctx, &item)
|
||||
+ if err != nil {
|
||||
+ d.log.WithError(err).WithField("pod", item.GetName()).Warn("failed to get VM name from pod")
|
||||
+ // Continue with empty vmName - will be caught by strict mode check
|
||||
+ vmName = ""
|
||||
+ }
|
||||
+
|
||||
+ podsUsingPVC = append(podsUsingPVC, podInfo{
|
||||
+ name: item.GetName(),
|
||||
+ vmName: vmName,
|
||||
+ })
|
||||
+
|
||||
+ break
|
||||
+ // Extract VM name, handling both regular and hotplug disk pods
|
||||
+ vmName, err := GetVMNameFromPod(ctx, kubeClient, log, &item)
|
||||
+ if err != nil {
|
||||
+ log.WithError(err).WithField("pod", item.GetName()).Warn("failed to get VM name from pod")
|
||||
+ // Continue with empty vmName - will be caught by strict mode check
|
||||
+ vmName = ""
|
||||
+ }
|
||||
+
|
||||
+ podsUsingPVC = append(podsUsingPVC, podInfo{
|
||||
+ name: item.GetName(),
|
||||
+ vmName: vmName,
|
||||
+ })
|
||||
+
|
||||
+ break
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
|
|
@ -168,7 +267,7 @@ index bea69a8..69e71a6 100644
|
|||
+ if len(podsUsingPVC) <= 1 {
|
||||
+ // Return VM name if there's exactly one pod
|
||||
+ if len(podsUsingPVC) == 1 {
|
||||
+ d.log.WithFields(logrus.Fields{
|
||||
+ log.WithFields(logrus.Fields{
|
||||
+ "volumeID": volumeID,
|
||||
+ "vmName": podsUsingPVC[0].vmName,
|
||||
+ "podCount": 1,
|
||||
|
|
@ -179,7 +278,7 @@ index bea69a8..69e71a6 100644
|
|||
+ return podsUsingPVC[0].vmName, nil
|
||||
+ }
|
||||
+
|
||||
+ d.log.WithFields(logrus.Fields{
|
||||
+ log.WithFields(logrus.Fields{
|
||||
+ "volumeID": volumeID,
|
||||
+ "pvcNamespace": pvcNamespace,
|
||||
+ "pvcName": pvcName,
|
||||
|
|
@ -209,7 +308,7 @@ index bea69a8..69e71a6 100644
|
|||
+ }
|
||||
+ }
|
||||
+
|
||||
+ d.log.WithFields(logrus.Fields{
|
||||
+ log.WithFields(logrus.Fields{
|
||||
+ "pvcNamespace": pvcNamespace,
|
||||
+ "pvcName": pvcName,
|
||||
+ "vmName": vmName,
|
||||
|
|
@ -219,30 +318,62 @@ index bea69a8..69e71a6 100644
|
|||
+ return vmName, nil
|
||||
+}
|
||||
+
|
||||
// ControllerPublishVolume https://github.com/container-storage-interface/spec/blob/v1.9.0/spec.md#controllerpublishvolume
|
||||
func (d Driver) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) {
|
||||
if req.GetVolumeId() == "" {
|
||||
@@ -751,6 +964,14 @@ func (d Driver) ControllerPublishVolume(ctx context.Context, req *csi.Controller
|
||||
// ReadWriteMany block volume
|
||||
rwxBlock := req.VolumeCapability.AccessMode.GetMode() == csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER && req.VolumeCapability.GetBlock() != nil
|
||||
|
||||
+ // Validate RWX block attachment to prevent misuse of allow-two-primaries
|
||||
+ if rwxBlock {
|
||||
+ if _, err := d.validateRWXBlockAttachment(ctx, req.GetVolumeId()); err != nil {
|
||||
+ return nil, status.Errorf(codes.FailedPrecondition,
|
||||
+ "ControllerPublishVolume failed for %s: %v", req.GetVolumeId(), err)
|
||||
+ }
|
||||
+// GetVMNameFromPod extracts the VM name from a pod, handling both regular virt-launcher pods
|
||||
+// and hotplug disk pods (which reference the virt-launcher pod via ownerReferences).
|
||||
+func GetVMNameFromPod(ctx context.Context, kubeClient dynamic.Interface, log *logrus.Entry, pod *unstructured.Unstructured) (string, error) {
|
||||
+ labels := pod.GetLabels()
|
||||
+ if labels == nil {
|
||||
+ return "", nil
|
||||
+ }
|
||||
+
|
||||
devPath, err := d.Assignments.Attach(ctx, req.GetVolumeId(), req.GetNodeId(), rwxBlock)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal,
|
||||
diff --git a/pkg/driver/rwx_validation_test.go b/pkg/driver/rwx_validation_test.go
|
||||
+ // Direct case: pod has vm.kubevirt.io/name label (virt-launcher pod)
|
||||
+ if vmName, ok := labels[KubeVirtVMLabel]; ok && vmName != "" {
|
||||
+ return vmName, nil
|
||||
+ }
|
||||
+
|
||||
+ // Hotplug disk case: pod has kubevirt.io: hotplug-disk label
|
||||
+ // Follow ownerReferences to find the virt-launcher pod
|
||||
+ if hotplugValue, ok := labels[KubeVirtHotplugDiskLabel]; ok && hotplugValue == "hotplug-disk" {
|
||||
+ ownerRefs := pod.GetOwnerReferences()
|
||||
+ for _, owner := range ownerRefs {
|
||||
+ if owner.Kind != "Pod" || owner.Controller == nil || !*owner.Controller {
|
||||
+ continue
|
||||
+ }
|
||||
+
|
||||
+ // Get the owner pod (virt-launcher)
|
||||
+ ownerPod, err := kubeClient.Resource(PodGVR).Namespace(pod.GetNamespace()).Get(ctx, owner.Name, metav1.GetOptions{})
|
||||
+ if err != nil {
|
||||
+ return "", fmt.Errorf("failed to get owner pod %s: %w", owner.Name, err)
|
||||
+ }
|
||||
+
|
||||
+ // Extract VM name from owner pod
|
||||
+ ownerLabels := ownerPod.GetLabels()
|
||||
+ if ownerLabels != nil {
|
||||
+ if vmName, ok := ownerLabels[KubeVirtVMLabel]; ok && vmName != "" {
|
||||
+ log.WithFields(logrus.Fields{
|
||||
+ "hotplugPod": pod.GetName(),
|
||||
+ "virtLauncher": owner.Name,
|
||||
+ "vmName": vmName,
|
||||
+ }).Debug("resolved VM name from hotplug disk pod via owner reference")
|
||||
+
|
||||
+ return vmName, nil
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return "", fmt.Errorf("owner pod %s does not have %s label", owner.Name, KubeVirtVMLabel)
|
||||
+ }
|
||||
+
|
||||
+ return "", fmt.Errorf("hotplug disk pod %s has no controller owner reference", pod.GetName())
|
||||
+ }
|
||||
+
|
||||
+ return "", nil
|
||||
+}
|
||||
diff --git a/pkg/utils/rwx_validation_test.go b/pkg/utils/rwx_validation_test.go
|
||||
new file mode 100644
|
||||
index 0000000..92c1046
|
||||
index 00000000..d75690f9
|
||||
--- /dev/null
|
||||
+++ b/pkg/driver/rwx_validation_test.go
|
||||
@@ -0,0 +1,353 @@
|
||||
+++ b/pkg/utils/rwx_validation_test.go
|
||||
@@ -0,0 +1,342 @@
|
||||
+/*
|
||||
+CSI Driver for Linstor
|
||||
+Copyright © 2018 LINBIT USA, LLC
|
||||
|
|
@ -261,7 +392,7 @@ index 0000000..92c1046
|
|||
+along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
+*/
|
||||
+
|
||||
+package driver
|
||||
+package utils
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
|
|
@ -424,22 +555,17 @@ index 0000000..92c1046
|
|||
+ }
|
||||
+
|
||||
+ gvrToListKind := map[schema.GroupVersionResource]string{
|
||||
+ podGVR: "PodList",
|
||||
+ pvGVR: "PersistentVolumeList",
|
||||
+ PodGVR: "PodList",
|
||||
+ PVGVR: "PersistentVolumeList",
|
||||
+ }
|
||||
+ client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, objects...)
|
||||
+
|
||||
+ // Create driver with fake client
|
||||
+ // Create logger
|
||||
+ logger := logrus.NewEntry(logrus.New())
|
||||
+ logger.Logger.SetLevel(logrus.DebugLevel)
|
||||
+
|
||||
+ driver := &Driver{
|
||||
+ kubeClient: client,
|
||||
+ log: logger,
|
||||
+ }
|
||||
+
|
||||
+ // Run validation
|
||||
+ vmName, err := driver.validateRWXBlockAttachment(context.Background(), "test-volume-id")
|
||||
+ vmName, err := ValidateRWXBlockAttachment(context.Background(), client, logger, "test-volume-id")
|
||||
+
|
||||
+ if tc.expectError {
|
||||
+ assert.Error(t, err)
|
||||
|
|
@ -461,12 +587,8 @@ index 0000000..92c1046
|
|||
+func TestValidateRWXBlockAttachmentNoKubeClient(t *testing.T) {
|
||||
+ // When not running in Kubernetes (no client), validation should be skipped
|
||||
+ logger := logrus.NewEntry(logrus.New())
|
||||
+ driver := &Driver{
|
||||
+ kubeClient: nil,
|
||||
+ log: logger,
|
||||
+ }
|
||||
+
|
||||
+ vmName, err := driver.validateRWXBlockAttachment(context.Background(), "test-volume-id")
|
||||
+ vmName, err := ValidateRWXBlockAttachment(context.Background(), nil, logger, "test-volume-id")
|
||||
+ assert.NoError(t, err)
|
||||
+ assert.Empty(t, vmName)
|
||||
+}
|
||||
|
|
@ -476,20 +598,15 @@ index 0000000..92c1046
|
|||
+ scheme := runtime.NewScheme()
|
||||
+
|
||||
+ gvrToListKind := map[schema.GroupVersionResource]string{
|
||||
+ podGVR: "PodList",
|
||||
+ pvGVR: "PersistentVolumeList",
|
||||
+ PodGVR: "PodList",
|
||||
+ PVGVR: "PersistentVolumeList",
|
||||
+ }
|
||||
+ client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind)
|
||||
+
|
||||
+ logger := logrus.NewEntry(logrus.New())
|
||||
+ logger.Logger.SetLevel(logrus.DebugLevel)
|
||||
+
|
||||
+ driver := &Driver{
|
||||
+ kubeClient: client,
|
||||
+ log: logger,
|
||||
+ }
|
||||
+
|
||||
+ vmName, err := driver.validateRWXBlockAttachment(context.Background(), "non-existent-pv")
|
||||
+ vmName, err := ValidateRWXBlockAttachment(context.Background(), client, logger, "non-existent-pv")
|
||||
+ assert.NoError(t, err)
|
||||
+ assert.Empty(t, vmName)
|
||||
+}
|
||||
|
|
@ -538,6 +655,9 @@ index 0000000..92c1046
|
|||
+ "name": pvcName,
|
||||
+ "namespace": pvcNamespace,
|
||||
+ },
|
||||
+ "csi": map[string]interface{}{
|
||||
+ "volumeHandle": name,
|
||||
+ },
|
||||
+ },
|
||||
+ },
|
||||
+ }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue