k8s.io/kubernetes@v1.29.3/pkg/volume/util/volumepathhandler/volume_path_handler_linux.go (about)

     1  //go:build linux
     2  // +build linux
     3  
     4  /*
     5  Copyright 2018 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package volumepathhandler
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"golang.org/x/sys/unix"
    31  
    32  	"k8s.io/apimachinery/pkg/types"
    33  	"k8s.io/klog/v2"
    34  )
    35  
    36  // AttachFileDevice takes a path to a regular file and makes it available as an
    37  // attached block device.
    38  func (v VolumePathHandler) AttachFileDevice(path string) (string, error) {
    39  	blockDevicePath, err := v.GetLoopDevice(path)
    40  	if err != nil && err.Error() != ErrDeviceNotFound {
    41  		return "", fmt.Errorf("GetLoopDevice failed for path %s: %v", path, err)
    42  	}
    43  
    44  	// If no existing loop device for the path, create one
    45  	if blockDevicePath == "" {
    46  		klog.V(4).Infof("Creating device for path: %s", path)
    47  		blockDevicePath, err = makeLoopDevice(path)
    48  		if err != nil {
    49  			return "", fmt.Errorf("makeLoopDevice failed for path %s: %v", path, err)
    50  		}
    51  	}
    52  	return blockDevicePath, nil
    53  }
    54  
    55  // DetachFileDevice takes a path to the attached block device and
    56  // detach it from block device.
    57  func (v VolumePathHandler) DetachFileDevice(path string) error {
    58  	loopPath, err := v.GetLoopDevice(path)
    59  	if err != nil {
    60  		if err.Error() == ErrDeviceNotFound {
    61  			klog.Warningf("couldn't find loopback device which takes file descriptor lock. Skip detaching device. device path: %q", path)
    62  		} else {
    63  			return fmt.Errorf("GetLoopDevice failed for path %s: %v", path, err)
    64  		}
    65  	} else {
    66  		if len(loopPath) != 0 {
    67  			err = removeLoopDevice(loopPath)
    68  			if err != nil {
    69  				return fmt.Errorf("removeLoopDevice failed for path %s: %v", path, err)
    70  			}
    71  		}
    72  	}
    73  	return nil
    74  }
    75  
    76  // GetLoopDevice returns the full path to the loop device associated with the given path.
    77  func (v VolumePathHandler) GetLoopDevice(path string) (string, error) {
    78  	_, err := os.Stat(path)
    79  	if os.IsNotExist(err) {
    80  		return "", errors.New(ErrDeviceNotFound)
    81  	}
    82  	if err != nil {
    83  		return "", fmt.Errorf("not attachable: %v", err)
    84  	}
    85  
    86  	return getLoopDeviceFromSysfs(path)
    87  }
    88  
    89  func makeLoopDevice(path string) (string, error) {
    90  	args := []string{"-f", path}
    91  	cmd := exec.Command(losetupPath, args...)
    92  
    93  	out, err := cmd.CombinedOutput()
    94  	if err != nil {
    95  		klog.V(2).Infof("Failed device create command for path: %s %v %s", path, err, out)
    96  		return "", fmt.Errorf("losetup %s failed: %v", strings.Join(args, " "), err)
    97  	}
    98  
    99  	return getLoopDeviceFromSysfs(path)
   100  }
   101  
   102  // removeLoopDevice removes specified loopback device
   103  func removeLoopDevice(device string) error {
   104  	args := []string{"-d", device}
   105  	cmd := exec.Command(losetupPath, args...)
   106  	out, err := cmd.CombinedOutput()
   107  	if err != nil {
   108  		if _, err := os.Stat(device); os.IsNotExist(err) {
   109  			return nil
   110  		}
   111  		klog.V(2).Infof("Failed to remove loopback device: %s: %v %s", device, err, out)
   112  		return fmt.Errorf("losetup -d %s failed: %v", device, err)
   113  	}
   114  	return nil
   115  }
   116  
   117  // getLoopDeviceFromSysfs finds the backing file for a loop
   118  // device from sysfs via "/sys/block/loop*/loop/backing_file".
   119  func getLoopDeviceFromSysfs(path string) (string, error) {
   120  	// If the file is a symlink.
   121  	realPath, err := filepath.EvalSymlinks(path)
   122  	if err != nil {
   123  		return "", fmt.Errorf("failed to evaluate path %s: %s", path, err)
   124  	}
   125  
   126  	devices, err := filepath.Glob("/sys/block/loop*")
   127  	if err != nil {
   128  		return "", fmt.Errorf("failed to list loop devices in sysfs: %s", err)
   129  	}
   130  
   131  	for _, device := range devices {
   132  		backingFile := fmt.Sprintf("%s/loop/backing_file", device)
   133  
   134  		// The contents of this file is the absolute path of "path".
   135  		data, err := os.ReadFile(backingFile)
   136  		if err != nil {
   137  			continue
   138  		}
   139  
   140  		// Return the first match.
   141  		backingFilePath := cleanBackingFilePath(string(data))
   142  		if backingFilePath == path || backingFilePath == realPath {
   143  			return fmt.Sprintf("/dev/%s", filepath.Base(device)), nil
   144  		}
   145  	}
   146  
   147  	return "", errors.New(ErrDeviceNotFound)
   148  }
   149  
   150  // cleanPath remove any trailing substrings that are not part of the backing file path.
   151  func cleanBackingFilePath(path string) string {
   152  	// If the block device was deleted, the path will contain a "(deleted)" suffix
   153  	path = strings.TrimSpace(path)
   154  	path = strings.TrimSuffix(path, "(deleted)")
   155  	return strings.TrimSpace(path)
   156  }
   157  
   158  // FindGlobalMapPathUUIDFromPod finds {pod uuid} bind mount under globalMapPath
   159  // corresponding to map path symlink, and then return global map path with pod uuid.
   160  // (See pkg/volume/volume.go for details on a global map path and a pod device map path.)
   161  // ex. mapPath symlink: pods/{podUid}}/{DefaultKubeletVolumeDevicesDirName}/{escapeQualifiedPluginName}/{volumeName} -> /dev/sdX
   162  //
   163  //	globalMapPath/{pod uuid} bind mount: plugins/kubernetes.io/{PluginName}/{DefaultKubeletVolumeDevicesDirName}/{volumePluginDependentPath}/{pod uuid} -> /dev/sdX
   164  func (v VolumePathHandler) FindGlobalMapPathUUIDFromPod(pluginDir, mapPath string, podUID types.UID) (string, error) {
   165  	var globalMapPathUUID string
   166  	// Find symbolic link named pod uuid under plugin dir
   167  	err := filepath.Walk(pluginDir, func(path string, fi os.FileInfo, err error) error {
   168  		if err != nil {
   169  			return err
   170  		}
   171  		if (fi.Mode()&os.ModeDevice == os.ModeDevice) && (fi.Name() == string(podUID)) {
   172  			klog.V(5).Infof("FindGlobalMapPathFromPod: path %s, mapPath %s", path, mapPath)
   173  			if res, err := compareBindMountAndSymlinks(path, mapPath); err == nil && res {
   174  				globalMapPathUUID = path
   175  			}
   176  		}
   177  		return nil
   178  	})
   179  	if err != nil {
   180  		return "", fmt.Errorf("FindGlobalMapPathUUIDFromPod failed: %v", err)
   181  	}
   182  	klog.V(5).Infof("FindGlobalMapPathFromPod: globalMapPathUUID %s", globalMapPathUUID)
   183  	// Return path contains global map path + {pod uuid}
   184  	return globalMapPathUUID, nil
   185  }
   186  
   187  // compareBindMountAndSymlinks returns if global path (bind mount) and
   188  // pod path (symlink) are pointing to the same device.
   189  // If there is an error in checking it returns error.
   190  func compareBindMountAndSymlinks(global, pod string) (bool, error) {
   191  	// To check if bind mount and symlink are pointing to the same device,
   192  	// we need to check if they are pointing to the devices that have same major/minor number.
   193  
   194  	// Get the major/minor number for global path
   195  	devNumGlobal, err := getDeviceMajorMinor(global)
   196  	if err != nil {
   197  		return false, fmt.Errorf("getDeviceMajorMinor failed for path %s: %v", global, err)
   198  	}
   199  
   200  	// Get the symlinked device from the pod path
   201  	devPod, err := os.Readlink(pod)
   202  	if err != nil {
   203  		return false, fmt.Errorf("failed to readlink path %s: %v", pod, err)
   204  	}
   205  	// Get the major/minor number for the symlinked device from the pod path
   206  	devNumPod, err := getDeviceMajorMinor(devPod)
   207  	if err != nil {
   208  		return false, fmt.Errorf("getDeviceMajorMinor failed for path %s: %v", devPod, err)
   209  	}
   210  	klog.V(5).Infof("CompareBindMountAndSymlinks: devNumGlobal %s, devNumPod %s", devNumGlobal, devNumPod)
   211  
   212  	// Check if the major/minor number are the same
   213  	if devNumGlobal == devNumPod {
   214  		return true, nil
   215  	}
   216  	return false, nil
   217  }
   218  
   219  // getDeviceMajorMinor returns major/minor number for the path with below format:
   220  // major:minor (in hex)
   221  // ex)
   222  //
   223  //	fc:10
   224  func getDeviceMajorMinor(path string) (string, error) {
   225  	var stat unix.Stat_t
   226  
   227  	if err := unix.Stat(path, &stat); err != nil {
   228  		return "", fmt.Errorf("failed to stat path %s: %v", path, err)
   229  	}
   230  
   231  	devNumber := uint64(stat.Rdev)
   232  	major := unix.Major(devNumber)
   233  	minor := unix.Minor(devNumber)
   234  
   235  	return fmt.Sprintf("%x:%x", major, minor), nil
   236  }