• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubevirt / kubevirt / 717b8003-5b59-444d-bc4d-5099b6c3f635

17 Jul 2026 11:28AM UTC coverage: 72.447%. Remained the same
717b8003-5b59-444d-bc4d-5099b6c3f635

push

prow

web-flow
Merge pull request #18044 from maxwmsft/fix-hotplug-volume-detach-deadlock

Fix hotplug volume detach deadlock in virt-handler

10 of 10 new or added lines in 1 file covered. (100.0%)

91 existing lines in 2 files now uncovered.

84123 of 116116 relevant lines covered (72.45%)

363.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.66
/pkg/virt-handler/hotplug-disk/mount.go
1
/*
2
 * This file is part of the KubeVirt project
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 *
16
 * Copyright The KubeVirt Authors.
17
 *
18
 */
19

20
package hotplug_volume
21

22
import (
23
        "errors"
24
        "fmt"
25
        "os"
26
        "path/filepath"
27
        "sync"
28
        "syscall"
29

30
        "kubevirt.io/kubevirt/pkg/checkpoint"
31
        "kubevirt.io/kubevirt/pkg/unsafepath"
32

33
        "golang.org/x/sys/unix"
34

35
        "kubevirt.io/kubevirt/pkg/safepath"
36
        virt_chroot "kubevirt.io/kubevirt/pkg/virt-handler/virt-chroot"
37

38
        diskutils "kubevirt.io/kubevirt/pkg/ephemeral-disk-utils"
39
        hotplugdisk "kubevirt.io/kubevirt/pkg/hotplug-disk"
40
        storagetypes "kubevirt.io/kubevirt/pkg/storage/types"
41
        "kubevirt.io/kubevirt/pkg/virt-handler/cgroup"
42
        "kubevirt.io/kubevirt/pkg/virt-handler/isolation"
43

44
        "github.com/opencontainers/cgroups"
45

46
        devices "github.com/opencontainers/cgroups/devices/config"
47
        "k8s.io/apimachinery/pkg/types"
48
        "k8s.io/apimachinery/pkg/util/sets"
49

50
        v1 "kubevirt.io/api/core/v1"
51
        "kubevirt.io/client-go/log"
52
)
53

54
//go:generate mockgen -source $GOFILE -package=$GOPACKAGE -destination=generated_mock_$GOFILE
55

56
var ErrWaitingForHotplugMount = errors.New("waiting for hotplug mount")
57

58
const (
59
        unableFindHotplugMountedDir = "unable to find hotplug mounted directories for vmi without uid"
60
)
61

62
var (
63
        nodeIsolationResult = func() isolation.IsolationResult {
×
64
                return isolation.NodeIsolationResult()
×
65
        }
×
66
        deviceBasePath = func(podUID types.UID, kubeletPodsDir string) (*safepath.Path, error) {
×
67
                return safepath.JoinAndResolveWithRelativeRoot("/proc/1/root", kubeletPodsDir, fmt.Sprintf("/%s/volumes/kubernetes.io~empty-dir/hotplug-disks", string(podUID)))
×
68
        }
×
69

70
        socketPath = func(podUID types.UID) string {
12✔
71
                return fmt.Sprintf("/pods/%s/volumes/kubernetes.io~empty-dir/hotplug-disks/hp.sock", string(podUID))
12✔
72
        }
12✔
73

74
        statDevice = func(fileName *safepath.Path) (os.FileInfo, error) {
×
75
                info, err := safepath.StatAtNoFollow(fileName)
×
76
                if err != nil {
×
77
                        return nil, err
×
78
                }
×
79
                if info.Mode()&os.ModeDevice == 0 {
×
80
                        return info, fmt.Errorf("%v is not a block device", fileName)
×
81
                }
×
82
                return info, nil
×
83
        }
84

85
        statSourceDevice = func(fileName *safepath.Path) (os.FileInfo, error) {
×
86
                // we don't know the device name, we only know that it is the only
×
87
                // device in a specific directory, let's look it up
×
88
                var devName string
×
89
                err := fileName.ExecuteNoFollow(func(safePath string) error {
×
90
                        entries, err := os.ReadDir(safePath)
×
91
                        if err != nil {
×
92
                                return err
×
93
                        }
×
94
                        for _, entry := range entries {
×
95
                                info, err := entry.Info()
×
96
                                if err != nil {
×
97
                                        return err
×
98
                                }
×
99
                                if info.Mode()&os.ModeDevice == 0 {
×
100
                                        // not a device
×
101
                                        continue
×
102
                                }
103
                                devName = entry.Name()
×
104
                                return nil
×
105
                        }
106
                        return fmt.Errorf("no device in %v", fileName)
×
107
                })
108
                if err != nil {
×
109
                        return nil, err
×
110
                }
×
111
                devPath, err := safepath.JoinNoFollow(fileName, devName)
×
112
                if err != nil {
×
113
                        return nil, err
×
114
                }
×
115
                return statDevice(devPath)
×
116
        }
117

118
        mknodCommand = func(basePath *safepath.Path, deviceName string, dev uint64, blockDevicePermissions os.FileMode) error {
×
119
                return safepath.MknodAtNoFollow(basePath, deviceName, blockDevicePermissions|syscall.S_IFBLK, dev)
×
120
        }
×
121

122
        mountCommand = func(sourcePath, targetPath *safepath.Path) ([]byte, error) {
×
123
                return virt_chroot.MountChroot(sourcePath, targetPath, false).CombinedOutput()
×
124
        }
×
125

126
        unmountCommand = func(diskPath *safepath.Path) ([]byte, error) {
×
127
                return virt_chroot.UmountChroot(diskPath).CombinedOutput()
×
128
        }
×
129

130
        isMounted = func(path *safepath.Path) (bool, error) {
8✔
131
                return isolation.IsMounted(path)
8✔
132
        }
8✔
133

134
        isBlockDevice = func(path *safepath.Path) (bool, error) {
5✔
135
                return isolation.IsBlockDevice(path)
5✔
136
        }
5✔
137

138
        isolationDetector = func() isolation.PodIsolationDetector {
×
139
                return isolation.NewSocketBasedIsolationDetector()
×
140
        }
×
141

142
        parentPathForMount = func(
143
                parent isolation.IsolationResult,
144
                child isolation.IsolationResult,
145
                findmntInfo FindmntInfo,
146
                podUID string,
147
                kubeletPodsDir string,
148
        ) (*safepath.Path, error) {
×
149
                return isolation.ParentPathForMount(parent, child, findmntInfo.Source, findmntInfo.Target, podUID, kubeletPodsDir)
×
150
        }
×
151
)
152

153
type volumeMounter struct {
154
        checkpointManager  checkpoint.CheckpointManager
155
        mountRecords       map[types.UID]*vmiMountTargetRecord
156
        mountRecordsLock   sync.Mutex
157
        skipSafetyCheck    bool
158
        hotplugDiskManager hotplugdisk.HotplugDiskManagerInterface
159
        ownershipManager   diskutils.OwnershipManagerInterface
160
        kubeletPodsDir     string
161
        host               string
162
}
163

164
// VolumeMounter is the interface used to mount and unmount volumes to/from a running virtlauncher pod.
165
type VolumeMounter interface {
166
        // Mount any new volumes defined in the VMI
167
        Mount(vmi *v1.VirtualMachineInstance, cgroupManager cgroup.Manager) error
168
        MountFromPod(vmi *v1.VirtualMachineInstance, sourceUID types.UID, cgroupManager cgroup.Manager) error
169
        // Unmount any volumes no longer defined in the VMI
170
        Unmount(vmi *v1.VirtualMachineInstance, cgroupManager cgroup.Manager) error
171
        //UnmountAll cleans up all hotplug volumes
172
        UnmountAll(vmi *v1.VirtualMachineInstance, cgroupManager cgroup.Manager) error
173
        //IsMounted returns if the volume is mounted or not.
174
        IsMounted(vmi *v1.VirtualMachineInstance, volume string, sourceUID types.UID) (bool, error)
175
}
176

177
type vmiMountTargetEntry struct {
178
        TargetFile string `json:"targetFile"`
179
}
180

181
type vmiMountTargetRecord struct {
182
        MountTargetEntries []vmiMountTargetEntry `json:"mountTargetEntries"`
183
        UsesSafePaths      bool                  `json:"usesSafePaths"`
184
}
185

186
func (r *vmiMountTargetRecord) appendPath(path string) bool {
13✔
187
        for _, entry := range r.MountTargetEntries {
17✔
188
                if entry.TargetFile == path {
5✔
189
                        return false // skip appending if already present
1✔
190
                }
1✔
191
        }
192
        r.MountTargetEntries = append(r.MountTargetEntries, vmiMountTargetEntry{TargetFile: path})
12✔
193
        return true
12✔
194
}
195

196
// NewVolumeMounter creates a new VolumeMounter
197
func NewVolumeMounter(mountStateDir string, kubeletPodsDir string, host string) VolumeMounter {
167✔
198
        return &volumeMounter{
167✔
199
                mountRecords:       make(map[types.UID]*vmiMountTargetRecord),
167✔
200
                checkpointManager:  checkpoint.NewSimpleCheckpointManager(mountStateDir),
167✔
201
                hotplugDiskManager: hotplugdisk.NewHotplugDiskManager(kubeletPodsDir),
167✔
202
                ownershipManager:   diskutils.DefaultOwnershipManager,
167✔
203
                kubeletPodsDir:     kubeletPodsDir,
167✔
204
                host:               host,
167✔
205
        }
167✔
206
}
167✔
207

208
func (m *volumeMounter) deleteMountTargetRecord(vmi *v1.VirtualMachineInstance) error {
4✔
209
        if string(vmi.UID) == "" {
4✔
210
                return fmt.Errorf(unableFindHotplugMountedDir)
×
211
        }
×
212

213
        record := vmiMountTargetRecord{}
4✔
214
        err := m.checkpointManager.Get(string(vmi.UID), &record)
4✔
215
        if err != nil && !errors.Is(err, os.ErrNotExist) {
4✔
216
                return fmt.Errorf("failed to get checkpoint %s, %w", vmi.UID, err)
×
217
        }
×
218

219
        if err == nil {
8✔
220
                for _, target := range record.MountTargetEntries {
9✔
221
                        os.Remove(target.TargetFile)
5✔
222
                }
5✔
223

224
                if err := m.checkpointManager.Delete(string(vmi.UID)); err != nil {
4✔
225
                        return fmt.Errorf("failed to delete checkpoint %s, %w", vmi.UID, err)
×
226
                }
×
227
        }
228

229
        m.mountRecordsLock.Lock()
4✔
230
        defer m.mountRecordsLock.Unlock()
4✔
231
        delete(m.mountRecords, vmi.UID)
4✔
232

4✔
233
        return nil
4✔
234
}
235

236
func (m *volumeMounter) getMountTargetRecord(vmi *v1.VirtualMachineInstance) (*vmiMountTargetRecord, error) {
18✔
237
        var ok bool
18✔
238
        var existingRecord *vmiMountTargetRecord
18✔
239

18✔
240
        if string(vmi.UID) == "" {
19✔
241
                return nil, fmt.Errorf(unableFindHotplugMountedDir)
1✔
242
        }
1✔
243

244
        m.mountRecordsLock.Lock()
17✔
245
        defer m.mountRecordsLock.Unlock()
17✔
246
        existingRecord, ok = m.mountRecords[vmi.UID]
17✔
247

17✔
248
        // first check memory cache
17✔
249
        if ok {
24✔
250
                return existingRecord, nil
7✔
251
        }
7✔
252

253
        // if not there, see if record is on disk, this can happen if virt-handler restarts
254
        record := vmiMountTargetRecord{}
10✔
255
        err := m.checkpointManager.Get(string(vmi.UID), &record)
10✔
256
        if err != nil && !errors.Is(err, os.ErrNotExist) {
10✔
257
                return nil, fmt.Errorf("failed to get checkpoint %s, %w", vmi.UID, err)
×
258
        }
×
259

260
        if err == nil {
10✔
261
                // XXX: backward compatibility for old unresolved paths, can be removed in July 2023
×
262
                // After a one-time convert and persist, old records are safe too.
×
263
                if !record.UsesSafePaths {
×
264
                        for i, path := range record.MountTargetEntries {
×
265
                                record.UsesSafePaths = true
×
266
                                safePath, err := safepath.JoinAndResolveWithRelativeRoot("/", path.TargetFile)
×
267
                                if err != nil {
×
268
                                        return nil, fmt.Errorf("failed converting legacy path to safepath: %v", err)
×
269
                                }
×
270
                                record.MountTargetEntries[i].TargetFile = unsafepath.UnsafeAbsolute(safePath.Raw())
×
271
                        }
272
                }
273

274
                m.mountRecords[vmi.UID] = &record
×
275
                return &record, nil
×
276
        }
277

278
        // not found
279
        return &vmiMountTargetRecord{UsesSafePaths: true}, nil
10✔
280
}
281

282
func (m *volumeMounter) setMountTargetRecord(vmi *v1.VirtualMachineInstance, record *vmiMountTargetRecord) error {
21✔
283
        if string(vmi.UID) == "" {
22✔
284
                return fmt.Errorf(unableFindHotplugMountedDir)
1✔
285
        }
1✔
286

287
        // XXX: backward compatibility for old unresolved paths, can be removed in July 2023
288
        // After a one-time convert and persist, old records are safe too.
289
        record.UsesSafePaths = true
20✔
290

20✔
291
        m.mountRecordsLock.Lock()
20✔
292
        defer m.mountRecordsLock.Unlock()
20✔
293

20✔
294
        if err := m.checkpointManager.Store(string(vmi.UID), record); err != nil {
20✔
295
                return fmt.Errorf("failed to checkpoint %s, %w", vmi.UID, err)
×
296
        }
×
297
        m.mountRecords[vmi.UID] = record
20✔
298
        return nil
20✔
299
}
300

301
func (m *volumeMounter) writePathToMountRecord(path string, vmi *v1.VirtualMachineInstance, record *vmiMountTargetRecord) error {
11✔
302
        if !record.appendPath(path) {
12✔
303
                return nil
1✔
304
        }
1✔
305
        if err := m.setMountTargetRecord(vmi, record); err != nil {
10✔
306
                return err
×
307
        }
×
308
        return nil
10✔
309
}
310

311
func (m *volumeMounter) mountHotplugVolume(
312
        vmi *v1.VirtualMachineInstance,
313
        volumeName string,
314
        sourceUID types.UID,
315
        record *vmiMountTargetRecord,
316
        mountDirectory bool,
317
        cgroupManager cgroup.Manager,
318
) error {
8✔
319
        logger := log.Log.Object(vmi)
8✔
320
        logger.V(4).Infof("Hotplug check volume name: %s", volumeName)
8✔
321
        if sourceUID != "" {
15✔
322
                if m.isBlockVolume(&vmi.Status, volumeName) {
12✔
323
                        logger.V(3).Infof("Mounting block volume: %s", volumeName)
5✔
324
                        if err := m.mountBlockHotplugVolume(vmi, volumeName, sourceUID, record, cgroupManager); err != nil {
5✔
325
                                return fmt.Errorf("failed to mount block hotplug volume %s: %w", volumeName, err)
×
326
                        }
×
327
                } else {
2✔
328
                        logger.V(3).Infof("Mounting file system volume: %s", volumeName)
2✔
329
                        if err := m.mountFileSystemHotplugVolume(vmi, volumeName, sourceUID, record, mountDirectory); err != nil {
2✔
330
                                return fmt.Errorf("failed to mount filesystem hotplug volume %s: %w", volumeName, err)
×
331
                        }
×
332
                }
333
        }
334
        return nil
8✔
335
}
336

337
func (m *volumeMounter) Mount(vmi *v1.VirtualMachineInstance, cgroupManager cgroup.Manager) error {
4✔
338
        return m.mountFromPod(vmi, "", cgroupManager)
4✔
339
}
4✔
340

341
func (m *volumeMounter) MountFromPod(vmi *v1.VirtualMachineInstance, sourceUID types.UID, cgroupManager cgroup.Manager) error {
×
342
        return m.mountFromPod(vmi, sourceUID, cgroupManager)
×
343
}
×
344

345
func (m *volumeMounter) mountFromPod(vmi *v1.VirtualMachineInstance, sourceUID types.UID, cgroupManager cgroup.Manager) error {
7✔
346
        record, err := m.getMountTargetRecord(vmi)
7✔
347
        if err != nil {
7✔
348
                return err
×
349
        }
×
350

351
        specVolumes := sets.New[string]()
7✔
352
        for i := range vmi.Spec.Volumes {
17✔
353
                specVolumes.Insert(vmi.Spec.Volumes[i].Name)
10✔
354
        }
10✔
355
        for i := range vmi.Spec.UtilityVolumes {
8✔
356
                specVolumes.Insert(vmi.Spec.UtilityVolumes[i].Name)
1✔
357
        }
1✔
358

359
        for _, volumeStatus := range vmi.Status.VolumeStatus {
21✔
360
                if volumeStatus.HotplugVolume == nil {
17✔
361
                        // Skip non hotplug volumes
3✔
362
                        continue
3✔
363
                }
364

365
                if !specVolumes.Has(volumeStatus.Name) {
13✔
366
                        log.Log.Object(vmi).V(3).Infof("Skipping mount for volume %s: no longer in VMI spec", volumeStatus.Name)
2✔
367
                        continue
2✔
368
                }
369

370
                if storagetypes.IsUtilityVolume(vmi, volumeStatus.Name) && m.isBlockVolume(&vmi.Status, volumeStatus.Name) {
10✔
371
                        log.Log.Object(vmi).Warningf("Skipping mount for utility volume %s: configured with block mode PVC, utility volumes require filesystem mode", volumeStatus.Name)
1✔
372
                        continue
1✔
373
                }
374

375
                mountDirectory := m.isDirectoryMounted(vmi, volumeStatus.Name)
8✔
376
                volumeSourceUID := sourceUID
8✔
377
                if volumeSourceUID == "" {
16✔
378
                        volumeSourceUID = volumeStatus.HotplugVolume.AttachPodUID
8✔
379
                }
8✔
380
                if err := m.mountHotplugVolume(vmi, volumeStatus.Name, volumeSourceUID, record, mountDirectory, cgroupManager); err != nil {
8✔
381
                        return err
×
382
                }
×
383
        }
384
        return nil
7✔
385
}
386

387
func (m *volumeMounter) isDirectoryMounted(vmi *v1.VirtualMachineInstance, volumeName string) bool {
9✔
388
        for _, utilityVolume := range vmi.Spec.UtilityVolumes {
9✔
389
                if utilityVolume.Name == volumeName {
×
390
                        return true
×
391
                }
×
392
        }
393
        for _, volume := range vmi.Spec.Volumes {
28✔
394
                if volume.Name == volumeName {
28✔
395
                        return volume.MemoryDump != nil
9✔
396
                }
9✔
397
        }
UNCOV
398
        return false
×
399
}
400

401
// isBlockVolume checks if the volumeDevices directory exists in the pod path, we assume there is a single volume associated with
402
// each pod, we use this knowledge to determine if we have a block volume or not.
403
func (m *volumeMounter) isBlockVolume(vmiStatus *v1.VirtualMachineInstanceStatus, volumeName string) bool {
14✔
404
        // First evaluate the migrated volumed. In the case of a migrated volume, virt-handler needs to understand if it needs to consider the
14✔
405
        // volume mode of the source or the destination. Therefore, it evaluates if its is running on the destiation host or otherwise, it is the source.
14✔
406
        isDstHost := false
14✔
407
        if vmiStatus.MigrationState != nil {
14✔
408
                isDstHost = vmiStatus.MigrationState.TargetNode == m.host
×
409
        }
×
410
        for _, migVol := range vmiStatus.MigratedVolumes {
14✔
411
                if migVol.VolumeName == volumeName {
×
412
                        if isDstHost && migVol.DestinationPVCInfo != nil {
×
413
                                return storagetypes.IsPVCBlock(migVol.DestinationPVCInfo.VolumeMode)
×
414
                        }
×
415
                        if !isDstHost && migVol.SourcePVCInfo != nil {
×
416
                                return storagetypes.IsPVCBlock(migVol.SourcePVCInfo.VolumeMode)
×
417
                        }
×
418
                }
419
        }
420
        // Check if the volumeDevices directory exists in the attachment pod, if so, its a block device, otherwise its file system.
421
        for _, status := range vmiStatus.VolumeStatus {
37✔
422
                if status.Name == volumeName {
36✔
423
                        return status.PersistentVolumeClaimInfo != nil && storagetypes.IsPVCBlock(status.PersistentVolumeClaimInfo.VolumeMode)
13✔
424
                }
13✔
425
        }
426
        return false
1✔
427
}
428

429
func (m *volumeMounter) mountBlockHotplugVolume(
430
        vmi *v1.VirtualMachineInstance,
431
        volume string,
432
        sourceUID types.UID,
433
        record *vmiMountTargetRecord,
434
        cgroupManager cgroup.Manager,
435
) error {
6✔
436
        virtlauncherUID := m.findVirtlauncherUID(vmi)
6✔
437
        if virtlauncherUID == "" {
6✔
438
                // This is not the node the pod is running on.
×
439
                return nil
×
440
        }
×
441
        targetPath, err := m.hotplugDiskManager.GetHotplugTargetPodPathOnHost(virtlauncherUID)
6✔
442
        if err != nil {
6✔
443
                return err
×
444
        }
×
445

446
        if _, err := safepath.JoinNoFollow(targetPath, volume); errors.Is(err, os.ErrNotExist) {
11✔
447
                dev, permissions, err := m.getSourceMajorMinor(sourceUID, volume)
5✔
448
                if err != nil {
5✔
449
                        return err
×
450
                }
×
451

452
                if err := m.writePathToMountRecord(filepath.Join(unsafepath.UnsafeAbsolute(targetPath.Raw()), volume), vmi, record); err != nil {
5✔
453
                        return err
×
454
                }
×
455

456
                if err := m.createBlockDeviceFile(targetPath, volume, dev, permissions); err != nil && !os.IsExist(err) {
5✔
457
                        return err
×
458
                }
×
459
                log.Log.Object(vmi).V(1).Infof("successfully created block device %s", volume)
5✔
460
        } else if err != nil {
1✔
461
                return err
×
462
        }
×
463

464
        devicePath, err := safepath.JoinNoFollow(targetPath, volume)
6✔
465
        if err != nil {
6✔
466
                return err
×
467
        }
×
468
        if isBlockExists, err := isBlockDevice(devicePath); err != nil {
6✔
469
                return err
×
470
        } else if !isBlockExists {
6✔
471
                return fmt.Errorf("target device %v exists but it is not a block device", devicePath)
×
472
        }
×
473

474
        dev, _, err := m.getBlockFileMajorMinor(devicePath, statDevice)
6✔
475
        if err != nil {
6✔
476
                return err
×
477
        }
×
478
        // allow block devices
479
        if err := m.allowBlockMajorMinor(dev, cgroupManager); err != nil {
6✔
480
                return err
×
481
        }
×
482

483
        return m.ownershipManager.SetFileOwnership(devicePath)
6✔
484
}
485

486
func (m *volumeMounter) getSourceMajorMinor(sourceUID types.UID, volumeName string) (uint64, os.FileMode, error) {
8✔
487
        basePath, err := deviceBasePath(sourceUID, m.kubeletPodsDir)
8✔
488
        if err != nil {
8✔
489
                return 0, 0, err
×
490
        }
×
491
        devicePath, err := basePath.AppendAndResolveWithRelativeRoot(volumeName)
8✔
492
        if err != nil {
10✔
493
                return 0, 0, err
2✔
494
        }
2✔
495
        return m.getBlockFileMajorMinor(devicePath, statSourceDevice)
6✔
496
}
497

498
func (m *volumeMounter) getBlockFileMajorMinor(devicePath *safepath.Path, getter func(fileName *safepath.Path) (os.FileInfo, error)) (uint64, os.FileMode, error) {
19✔
499
        fileInfo, err := getter(devicePath)
19✔
500
        if err != nil {
20✔
501
                return 0, 0, err
1✔
502
        }
1✔
503
        info := fileInfo.Sys().(*syscall.Stat_t)
18✔
504
        return info.Rdev, fileInfo.Mode(), nil
18✔
505
}
506

507
func (m *volumeMounter) removeBlockMajorMinor(dev uint64, cgroupManager cgroup.Manager) error {
5✔
508
        return m.updateBlockMajorMinor(dev, false, cgroupManager)
5✔
509
}
5✔
510

511
func (m *volumeMounter) allowBlockMajorMinor(dev uint64, cgroupManager cgroup.Manager) error {
7✔
512
        return m.updateBlockMajorMinor(dev, true, cgroupManager)
7✔
513
}
7✔
514

515
func (m *volumeMounter) updateBlockMajorMinor(dev uint64, allow bool, cgroupManager cgroup.Manager) error {
12✔
516
        deviceRule := &devices.Rule{
12✔
517
                Type:        devices.BlockDevice,
12✔
518
                Major:       int64(unix.Major(dev)),
12✔
519
                Minor:       int64(unix.Minor(dev)),
12✔
520
                Permissions: "rwm",
12✔
521
                Allow:       allow,
12✔
522
        }
12✔
523

12✔
524
        if cgroupManager == nil {
12✔
525
                return fmt.Errorf("failed to apply device rule %+v: cgroup manager is nil", *deviceRule)
×
526
        }
×
527

528
        err := cgroupManager.Set(&cgroups.Resources{
12✔
529
                Devices: []*devices.Rule{deviceRule},
12✔
530
        })
12✔
531

12✔
532
        if err != nil {
12✔
533
                log.Log.Errorf("cgroup %s had failed to set device rule. error: %v. rule: %+v", cgroupManager.GetCgroupVersion(), err, *deviceRule)
×
534
        } else {
12✔
535
                log.Log.Infof("cgroup %s device rule is set successfully. rule: %+v", cgroupManager.GetCgroupVersion(), *deviceRule)
12✔
536
        }
12✔
537

538
        return err
12✔
539
}
540

541
func (m *volumeMounter) createBlockDeviceFile(basePath *safepath.Path, deviceName string, dev uint64, blockDevicePermissions os.FileMode) error {
8✔
542
        if _, err := safepath.JoinNoFollow(basePath, deviceName); errors.Is(err, os.ErrNotExist) {
15✔
543
                return mknodCommand(basePath, deviceName, dev, blockDevicePermissions)
7✔
544
        } else {
8✔
545
                return err
1✔
546
        }
1✔
547
}
548

549
func (m *volumeMounter) mountFileSystemHotplugVolume(vmi *v1.VirtualMachineInstance, volume string, sourceUID types.UID, record *vmiMountTargetRecord, mountDirectory bool) error {
6✔
550
        virtlauncherUID := m.findVirtlauncherUID(vmi)
6✔
551
        if virtlauncherUID == "" {
6✔
552
                // This is not the node the pod is running on.
×
553
                return nil
×
554
        }
×
555
        var target *safepath.Path
6✔
556
        var err error
6✔
557
        if mountDirectory {
6✔
558
                target, err = m.hotplugDiskManager.GetFileSystemDirectoryTargetPathFromHostView(virtlauncherUID, volume, true)
×
559
        } else {
6✔
560
                target, err = m.hotplugDiskManager.GetFileSystemDiskTargetPathFromHostView(virtlauncherUID, volume, true)
6✔
561
        }
6✔
562
        if err != nil {
6✔
563
                return err
×
564
        }
×
565

566
        isMounted, err := isMounted(target)
6✔
567
        if err != nil {
6✔
568
                return fmt.Errorf("failed to determine if %s is already mounted: %v", target, err)
×
569
        }
×
570
        if !isMounted {
12✔
571
                sourcePath, err := m.getSourcePodFilePath(sourceUID, vmi, volume)
6✔
572
                if err != nil {
7✔
573
                        return fmt.Errorf("failed to get source path for volume %s from source pod %s: %v: %w", volume, sourceUID, err, ErrWaitingForHotplugMount)
1✔
574
                }
1✔
575
                if err := m.writePathToMountRecord(unsafepath.UnsafeAbsolute(target.Raw()), vmi, record); err != nil {
5✔
576
                        return err
×
577
                }
×
578
                if !mountDirectory {
10✔
579
                        sourcePath, err = sourcePath.AppendAndResolveWithRelativeRoot("disk.img")
5✔
580
                        if err != nil {
6✔
581
                                return err
1✔
582
                        }
1✔
583
                }
584
                if out, err := mountCommand(sourcePath, target); err != nil {
4✔
585
                        return fmt.Errorf("failed to bindmount hotplug volume source from %v to %v: %v : %v", sourcePath, target, string(out), err)
×
586
                }
×
587
                log.Log.Object(vmi).V(1).Infof("successfully mounted hotplug volume %s", volume)
4✔
588
        }
589

590
        return m.ownershipManager.SetFileOwnership(target)
4✔
591
}
592

593
func (m *volumeMounter) findVirtlauncherUID(vmi *v1.VirtualMachineInstance) (uid types.UID) {
19✔
594
        cnt := 0
19✔
595
        for podUID := range vmi.Status.ActivePods {
41✔
596
                _, err := m.hotplugDiskManager.GetHotplugTargetPodPathOnHost(podUID)
22✔
597
                if err == nil {
42✔
598
                        uid = podUID
20✔
599
                        cnt++
20✔
600
                }
20✔
601
        }
602
        if cnt == 1 {
37✔
603
                return
18✔
604
        }
18✔
605
        // Either no pods, or multiple pods, skip.
606
        return ""
1✔
607
}
608

609
func (m *volumeMounter) getSourcePodFilePath(sourceUID types.UID, vmi *v1.VirtualMachineInstance, volume string) (*safepath.Path, error) {
12✔
610
        iso := isolationDetector()
12✔
611
        isoRes, err := iso.DetectForSocket(socketPath(sourceUID))
12✔
612
        if err != nil {
14✔
613
                return nil, err
2✔
614
        }
2✔
615
        findmounts, err := LookupFindmntInfoByVolume(volume, isoRes.Pid())
10✔
616
        if err != nil {
11✔
617
                return nil, err
1✔
618
        }
1✔
619
        nodeIsoRes := nodeIsolationResult()
9✔
620
        mountRoot, err := nodeIsoRes.MountRoot()
9✔
621
        if err != nil {
9✔
622
                return nil, err
×
623
        }
×
624

625
        for _, findmnt := range findmounts {
18✔
626
                if filepath.Base(findmnt.Target) == volume {
16✔
627
                        source := findmnt.GetSourcePath()
7✔
628
                        path, err := parentPathForMount(nodeIsoRes, isoRes, findmnt, string(sourceUID), m.kubeletPodsDir)
7✔
629
                        exists := !errors.Is(err, os.ErrNotExist)
7✔
630
                        if err != nil && !errors.Is(err, os.ErrNotExist) {
7✔
631
                                return nil, err
×
632
                        }
×
633

634
                        isBlock := false
7✔
635
                        if exists {
14✔
636
                                isBlock, _ = isBlockDevice(path)
7✔
637
                        }
7✔
638

639
                        if !exists || isBlock {
7✔
640
                                // file not found, or block device, or directory check if we can find the mount.
×
641
                                deviceFindMnt, err := LookupFindmntInfoByDevice(source)
×
642
                                if err != nil {
×
643
                                        // Try the device found from the source
×
644
                                        deviceFindMnt, err = LookupFindmntInfoByDevice(findmnt.GetSourceDevice())
×
645
                                        if err != nil {
×
646
                                                return nil, err
×
647
                                        }
×
648
                                        // Check if the path was relative to the device.
649
                                        if !exists {
×
650
                                                return mountRoot.AppendAndResolveWithRelativeRoot(deviceFindMnt[0].Target, source)
×
651
                                        }
×
652
                                        return nil, err
×
653
                                }
654
                                return mountRoot.AppendAndResolveWithRelativeRoot(deviceFindMnt[0].Target)
×
655
                        } else {
7✔
656
                                return path, nil
7✔
657
                        }
7✔
658
                }
659
        }
660
        // Did not find the disk image file, return error
661
        return nil, fmt.Errorf("unable to find source disk image path for pod %s", sourceUID)
2✔
662
}
663

664
// Unmount unmounts all hotplug disk that are no longer part of the VMI
665
func (m *volumeMounter) Unmount(vmi *v1.VirtualMachineInstance, cgroupManager cgroup.Manager) error {
3✔
666
        if vmi.UID != "" {
6✔
667
                record, err := m.getMountTargetRecord(vmi)
3✔
668
                if err != nil {
3✔
669
                        return err
×
670
                } else if record == nil {
3✔
671
                        // no entries to unmount
×
672
                        return nil
×
673
                }
×
674
                if len(record.MountTargetEntries) == 0 {
3✔
675
                        return nil
×
676
                }
×
677

678
                currentHotplugPaths := make(map[string]types.UID)
3✔
679
                virtlauncherUID := m.findVirtlauncherUID(vmi)
3✔
680

3✔
681
                basePath, err := m.hotplugDiskManager.GetHotplugTargetPodPathOnHost(virtlauncherUID)
3✔
682
                if err != nil {
3✔
683
                        if errors.Is(err, os.ErrNotExist) {
×
684
                                // no mounts left, the base path does not even exist anymore
×
685
                                if err := m.deleteMountTargetRecord(vmi); err != nil {
×
686
                                        return fmt.Errorf("failed to delete mount target records: %v", err)
×
687
                                }
×
688
                                return nil
×
689
                        }
690
                        return err
×
691
                }
692
                // Ideally we would be looking at the actual disks in the domain but:
693
                // 1. The domain is built from the VMI spec
694
                // 2. The domain syncs before unmount is called
695
                // 3. Unmount will not get called if VMI sync fails
696
                // we should be good
697
                for _, volume := range vmi.Spec.Volumes {
6✔
698
                        if !storagetypes.IsHotplugVolume(&volume) {
5✔
699
                                continue
2✔
700
                        }
701
                        var path *safepath.Path
1✔
702
                        var err error
1✔
703
                        if m.isBlockVolume(&vmi.Status, volume.Name) {
1✔
704
                                path, err = safepath.JoinNoFollow(basePath, volume.Name)
×
705
                                if errors.Is(err, os.ErrNotExist) {
×
706
                                        // already unmounted or never mounted
×
707
                                        continue
×
708
                                }
709
                        } else if m.isDirectoryMounted(vmi, volume.Name) {
1✔
710
                                path, err = m.hotplugDiskManager.GetFileSystemDirectoryTargetPathFromHostView(virtlauncherUID, volume.Name, false)
×
711
                                if errors.Is(err, os.ErrNotExist) {
×
712
                                        // already unmounted or never mounted
×
713
                                        continue
×
714
                                }
715
                        } else {
1✔
716
                                path, err = m.hotplugDiskManager.GetFileSystemDiskTargetPathFromHostView(virtlauncherUID, volume.Name, false)
1✔
717
                                if errors.Is(err, os.ErrNotExist) {
1✔
718
                                        // already unmounted or never mounted
×
719
                                        continue
×
720
                                }
721
                        }
722
                        if err != nil {
1✔
723
                                return err
×
724
                        }
×
725
                        currentHotplugPaths[unsafepath.UnsafeAbsolute(path.Raw())] = virtlauncherUID
1✔
726
                }
727
                for _, utilityVolume := range vmi.Spec.UtilityVolumes {
3✔
728
                        if m.isBlockVolume(&vmi.Status, utilityVolume.Name) {
×
729
                                log.Log.Object(vmi).Warningf("Skipping unmount cleanup for utility volume %s: configured with block mode PVC", utilityVolume.Name)
×
730
                                continue
×
731
                        }
732

733
                        var path *safepath.Path
×
734
                        var err error
×
735
                        path, err = m.hotplugDiskManager.GetFileSystemDirectoryTargetPathFromHostView(virtlauncherUID, utilityVolume.Name, false)
×
736
                        if errors.Is(err, os.ErrNotExist) {
×
737
                                // already unmounted or never mounted
×
738
                                continue
×
739
                        }
740
                        if err != nil {
×
741
                                return err
×
742
                        }
×
743
                        currentHotplugPaths[unsafepath.UnsafeAbsolute(path.Raw())] = virtlauncherUID
×
744
                }
745
                newRecord := vmiMountTargetRecord{
3✔
746
                        MountTargetEntries: make([]vmiMountTargetEntry, 0),
3✔
747
                }
3✔
748
                var errs []error
3✔
749
                logErrAndKeepMount := func(path string, format string, args ...any) {
4✔
750
                        err := fmt.Errorf(format, args...)
1✔
751
                        log.Log.Object(vmi).Error(err.Error())
1✔
752
                        newRecord.appendPath(path)
1✔
753
                        errs = append(errs, err)
1✔
754
                }
1✔
755
                for _, entry := range record.MountTargetEntries {
8✔
756
                        fd, err := safepath.NewFileNoFollow(entry.TargetFile)
5✔
757
                        if err != nil {
5✔
758
                                if errors.Is(err, os.ErrNotExist) {
×
759
                                        log.Log.Object(vmi).Infof("Volume %v is not mounted anymore, continuing", entry.TargetFile)
×
760
                                } else {
×
761
                                        logErrAndKeepMount(entry.TargetFile, "Unable to unmount volume at path %s: %v", entry.TargetFile, err)
×
762
                                }
×
763
                                continue
×
764
                        }
765
                        fd.Close()
5✔
766
                        diskPath := fd.Path()
5✔
767
                        diskPathAbs := unsafepath.UnsafeAbsolute(diskPath.Raw())
5✔
768

5✔
769
                        if _, ok := currentHotplugPaths[diskPathAbs]; !ok {
9✔
770
                                if blockDevice, err := isBlockDevice(diskPath); err != nil {
5✔
771
                                        logErrAndKeepMount(diskPathAbs, "Unable to unmount volume at path %s: %v", diskPath, err)
1✔
772
                                        continue
1✔
773
                                } else if blockDevice {
4✔
774
                                        if err := m.unmountBlockHotplugVolumes(diskPath, cgroupManager); err != nil {
1✔
775
                                                logErrAndKeepMount(diskPathAbs, "Unable to remove block device at path %s: %v", diskPath, err)
×
776
                                                continue
×
777
                                        }
778
                                } else if err := m.unmountFileSystemHotplugVolumes(diskPath); err != nil {
2✔
779
                                        logErrAndKeepMount(diskPathAbs, "Unable to unmount filesystem volume at path %s: %v", diskPath, err)
×
780
                                        continue
×
781
                                }
782
                                log.Log.Object(vmi).V(3).Infof("Unmounted hotplug volume path %s", diskPath)
3✔
783
                        } else {
1✔
784
                                _ = newRecord.appendPath(diskPathAbs)
1✔
785
                        }
1✔
786
                }
787
                if len(newRecord.MountTargetEntries) > 0 {
5✔
788
                        err = m.setMountTargetRecord(vmi, &newRecord)
2✔
789
                } else {
3✔
790
                        err = m.deleteMountTargetRecord(vmi)
1✔
791
                }
1✔
792
                if err != nil {
3✔
793
                        return err
×
794
                }
×
795
                if len(errs) > 0 {
4✔
796
                        return fmt.Errorf("failed to cleanup hotplug mounts for VMI %s: %w", vmi.Name, errors.Join(errs...))
1✔
797
                }
1✔
798
        }
799
        return nil
2✔
800
}
801

802
func (m *volumeMounter) unmountFileSystemHotplugVolumes(diskPath *safepath.Path) error {
7✔
803
        if mounted, err := isMounted(diskPath); err != nil {
8✔
804
                return fmt.Errorf("failed to check mount point for hotplug disk %v: %v", diskPath, err)
1✔
805
        } else if mounted {
10✔
806
                out, err := unmountCommand(diskPath)
3✔
807
                if err != nil {
4✔
808
                        return fmt.Errorf("failed to unmount hotplug disk %v: %v : %v", diskPath, string(out), err)
1✔
809
                }
1✔
810
                err = safepath.UnlinkAtNoFollow(diskPath)
2✔
811
                if err != nil {
2✔
812
                        return fmt.Errorf("failed to remove hotplug disk directory %v: %v : %v", diskPath, string(out), err)
×
813
                }
×
814

815
        }
816
        return nil
5✔
817
}
818

819
func (m *volumeMounter) unmountBlockHotplugVolumes(diskPath *safepath.Path, cgroupManager cgroup.Manager) error {
5✔
820
        // Get major and minor so we can deny the container.
5✔
821
        dev, _, err := m.getBlockFileMajorMinor(diskPath, statDevice)
5✔
822
        if err != nil {
5✔
823
                return err
×
824
        }
×
825
        // Delete block device file
826
        if err := safepath.UnlinkAtNoFollow(diskPath); err != nil {
6✔
827
                return err
1✔
828
        }
1✔
829
        return m.removeBlockMajorMinor(dev, cgroupManager)
4✔
830
}
831

832
// UnmountAll unmounts all hotplug disks of a given VMI.
833
func (m *volumeMounter) UnmountAll(vmi *v1.VirtualMachineInstance, cgroupManager cgroup.Manager) error {
1✔
834
        if vmi.UID != "" {
2✔
835
                logger := log.DefaultLogger().Object(vmi)
1✔
836
                logger.Info("Cleaning up remaining hotplug volumes")
1✔
837
                record, err := m.getMountTargetRecord(vmi)
1✔
838
                if err != nil {
1✔
839
                        return err
×
840
                } else if record == nil {
1✔
841
                        // no entries to unmount
×
842
                        logger.Info("No hotplug volumes found to unmount")
×
843
                        return nil
×
844
                }
×
845

846
                for _, entry := range record.MountTargetEntries {
3✔
847
                        diskPath, err := safepath.NewFileNoFollow(entry.TargetFile)
2✔
848
                        if err != nil {
2✔
849
                                if errors.Is(err, os.ErrNotExist) {
×
850
                                        logger.Infof("Device %v is not mounted anymore, continuing.", entry.TargetFile)
×
851
                                        continue
×
852
                                }
853
                                logger.Warningf("Unable to unmount volume at path %s: %v", entry.TargetFile, err)
×
854
                                continue
×
855
                        }
856
                        diskPath.Close()
2✔
857
                        if isBlock, err := isBlockDevice(diskPath.Path()); err != nil {
2✔
858
                                logger.Warningf("Unable to unmount volume at path %s: %v", diskPath, err)
×
859
                        } else if isBlock {
3✔
860
                                if err := m.unmountBlockHotplugVolumes(diskPath.Path(), cgroupManager); err != nil {
1✔
861
                                        logger.Warningf("Unable to remove block device at path %s: %v", diskPath, err)
×
862
                                        // Don't return error, try next.
×
863
                                }
×
864
                        } else {
1✔
865
                                if err := m.unmountFileSystemHotplugVolumes(diskPath.Path()); err != nil {
1✔
866
                                        logger.Warningf("Unable to unmount volume at path %s: %v", diskPath, err)
×
867
                                        // Don't return error, try next.
×
868
                                }
×
869
                        }
870
                }
871
                err = m.deleteMountTargetRecord(vmi)
1✔
872
                if err != nil {
1✔
873
                        return err
×
874
                }
×
875
        }
876
        return nil
1✔
877
}
878

879
func (m *volumeMounter) IsMounted(vmi *v1.VirtualMachineInstance, volume string, sourceUID types.UID) (bool, error) {
×
880
        virtlauncherUID := m.findVirtlauncherUID(vmi)
×
881
        if virtlauncherUID == "" {
×
882
                // This is not the node the pod is running on.
×
883
                return false, fmt.Errorf("Unable to determine virt-launcher UID")
×
884
        }
×
885
        targetPath, err := m.hotplugDiskManager.GetHotplugTargetPodPathOnHost(virtlauncherUID)
×
886
        if err != nil {
×
887
                if errors.Is(err, os.ErrNotExist) {
×
888
                        return false, nil
×
889
                }
×
890
                return false, err
×
891
        }
892
        if m.isBlockVolume(&vmi.Status, volume) {
×
893
                deviceName, err := safepath.JoinNoFollow(targetPath, volume)
×
894
                if err != nil {
×
895
                        if errors.Is(err, os.ErrNotExist) {
×
896
                                return false, nil
×
897
                        }
×
898
                        return false, err
×
899
                }
900
                isBlockExists, _ := isBlockDevice(deviceName)
×
901
                return isBlockExists, nil
×
902
        }
903
        if m.isDirectoryMounted(vmi, volume) {
×
904
                path, err := safepath.JoinNoFollow(targetPath, volume)
×
905
                if err != nil {
×
906
                        if errors.Is(err, os.ErrNotExist) {
×
907
                                return false, nil
×
908
                        }
×
909
                        return false, err
×
910
                }
911
                return isMounted(path)
×
912
        }
913
        path, err := safepath.JoinNoFollow(targetPath, fmt.Sprintf("%s.img", volume))
×
914
        if err != nil {
×
915
                if errors.Is(err, os.ErrNotExist) {
×
916
                        return false, nil
×
917
                }
×
918
                return false, err
×
919
        }
920
        return isMounted(path)
×
921
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc