github.com/openebs/node-disk-manager@v1.9.1-0.20230225014141-4531f06ffa1e/pkg/mount/mountutil.go (about)

     1  /*
     2  Copyright 2019 The OpenEBS Authors.
     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  
    17  package mount
    18  
    19  import (
    20  	"bufio"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	"github.com/openebs/node-disk-manager/pkg/features"
    28  )
    29  
    30  var ErrCouldNotFindRootDevice = fmt.Errorf("could not find root device")
    31  
    32  const (
    33  	procCmdLine     = "/proc/cmdline"
    34  	hostProcCmdLine = "/host" + procCmdLine
    35  )
    36  
    37  var ErrAttributesNotFound error = fmt.Errorf("could not get device mount " +
    38  	"attributes, Path/MountPoint not present in mounts file")
    39  
    40  // DiskMountUtil contains the mountfile path, devpath/mountpoint which can be used to
    41  // detect partition of a mountpoint or mountpoint of a partition.
    42  type DiskMountUtil struct {
    43  	filePath   string
    44  	devPath    string
    45  	mountPoint string
    46  }
    47  
    48  type getMountData func(string) (DeviceMountAttr, bool)
    49  
    50  // NewMountUtil returns DiskMountUtil struct for given mounts file path and mount point
    51  func NewMountUtil(filePath, devPath, mountPoint string) DiskMountUtil {
    52  	MountUtil := DiskMountUtil{
    53  		filePath:   filePath,
    54  		devPath:    devPath,
    55  		mountPoint: mountPoint,
    56  	}
    57  	return MountUtil
    58  }
    59  
    60  // GetDiskPath returns os disk devpath
    61  func (m DiskMountUtil) GetDiskPath() (string, error) {
    62  	mountAttr, err := m.getDeviceMountAttr(m.getPartitionName)
    63  	if err != nil {
    64  		return "", err
    65  	}
    66  	devPath, err := getPartitionDevPath(mountAttr.DevPath)
    67  	if err != nil {
    68  		return "", err
    69  	}
    70  	_, err = filepath.EvalSymlinks(devPath)
    71  	if err != nil {
    72  		return "", err
    73  	}
    74  	return devPath, err
    75  }
    76  
    77  // getDeviceMountAttr read mounts file and returns device mount attributes, which includes partition name,
    78  // mountpoint and filesystem
    79  func (m DiskMountUtil) getDeviceMountAttr(fn getMountData) (DeviceMountAttr, error) {
    80  	found := false
    81  	mountAttr := DeviceMountAttr{}
    82  	// Read file from filepath and get which partition is mounted on given mount point
    83  	file, err := os.Open(m.filePath)
    84  	if err != nil {
    85  		return mountAttr, err
    86  	}
    87  	defer file.Close()
    88  	scanner := bufio.NewScanner(file)
    89  	if err := scanner.Err(); err != nil {
    90  		return mountAttr, err
    91  	}
    92  	for scanner.Scan() {
    93  		line := scanner.Text()
    94  
    95  		/*
    96  			read each line of given file in below format -
    97  			/dev/sda4 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
    98  			/dev/sda4 /var/lib/docker/aufs ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
    99  		*/
   100  
   101  		// we are interested only in lines that start with /dev
   102  		if !strings.HasPrefix(line, "/dev") {
   103  			continue
   104  		}
   105  		if lineMountAttr, ok := fn(line); ok {
   106  			found = true
   107  			mergeDeviceMountAttrs(&mountAttr, &lineMountAttr)
   108  		}
   109  	}
   110  	if found {
   111  		return mountAttr, nil
   112  	}
   113  	return mountAttr, ErrAttributesNotFound
   114  }
   115  
   116  //	getPartitionDevPath takes disk/partition name as input (sda, sda1, sdb, sdb2 ...) and
   117  //	returns dev path of that disk/partition (/dev/sda1,/dev/sda)
   118  //
   119  // NOTE: if the feature gate to use OS disk is enabled, the dev path of disk /partition is returned,
   120  // eg: sda1, sda2, root on sda5 returns /dev/sda1, /dev/sda2, /dev/sda5 respectively
   121  // else, the devpath of the parent disk will be returned
   122  // eg: sda1, root on sda5 returns /dev/sda, /dev/sda respectively
   123  func getPartitionDevPath(partition string) (string, error) {
   124  	softlink, err := getSoftLinkForPartition(partition)
   125  	if err != nil {
   126  		return "", err
   127  	}
   128  
   129  	link, err := filepath.EvalSymlinks(softlink)
   130  	if err != nil {
   131  		return "", err
   132  	}
   133  
   134  	var disk string
   135  	var ok bool
   136  	if features.FeatureGates.IsEnabled(features.UseOSDisk) {
   137  		// the last part will be used instead of the parent disk
   138  		// eg: /sys/devices/pci0000:00/0000:00:1f.2/ata1/host0/target0:0:0/0:0:0:0/block/sda/sda4 is the link
   139  		// and sda4 will be the device.
   140  		split := strings.Split(link, "/")
   141  		disk = split[len(split)-1]
   142  	} else {
   143  		disk, ok = getParentBlockDevice(link)
   144  		if !ok {
   145  			return "", fmt.Errorf("could not find parent device for %s", link)
   146  		}
   147  	}
   148  
   149  	return "/dev/" + disk, nil
   150  }
   151  
   152  //	getSoftLinkForPartition returns path to /sys/class/block/$partition
   153  //	if the path does not exist and the partition is "root"
   154  //	then the root partition is detected from /proc/cmdline
   155  func getSoftLinkForPartition(partition string) (string, error) {
   156  	softlink := getLinkForPartition(partition)
   157  
   158  	if !fileExists(softlink) && partition == "root" {
   159  		partition, err := getRootPartition()
   160  		if err != nil {
   161  			return "", err
   162  		}
   163  		softlink = getLinkForPartition(partition)
   164  	}
   165  	return softlink, nil
   166  }
   167  
   168  //	getLinkForPartition returns path to sys block path
   169  func getLinkForPartition(partition string) string {
   170  	// dev path be like /dev/sda4 we need to remove /dev/ from this string to get sys block path.
   171  	return "/sys/class/block/" + partition
   172  }
   173  
   174  //	getRootPartition resolves link /dev/root using /proc/cmdline
   175  func getRootPartition() (string, error) {
   176  	file, err := os.Open(getCmdlineFile())
   177  	if err != nil {
   178  		return "", err
   179  	}
   180  	defer file.Close()
   181  
   182  	path, err := parseRootDeviceLink(file)
   183  	if err != nil {
   184  		return "", err
   185  	}
   186  
   187  	link, err := filepath.EvalSymlinks(path)
   188  	if err != nil {
   189  		return "", err
   190  	}
   191  
   192  	return getDeviceName(link), nil
   193  }
   194  
   195  func parseRootDeviceLink(file io.Reader) (string, error) {
   196  	scanner := bufio.NewScanner(file)
   197  	if err := scanner.Err(); err != nil {
   198  		return "", err
   199  	}
   200  
   201  	rootPrefix := "root="
   202  	for scanner.Scan() {
   203  		line := strings.TrimSpace(scanner.Text())
   204  
   205  		if line == "" {
   206  			continue
   207  		}
   208  
   209  		args := strings.Split(line, " ")
   210  
   211  		// looking for root device identification
   212  		// ... root=UUID=d41162ba-25e4-4c44-8793-2abef96d27e9 ...
   213  		for _, arg := range args {
   214  			if !strings.HasPrefix(arg, rootPrefix) {
   215  				continue
   216  			}
   217  
   218  			rootSpec := strings.Split(arg[len(rootPrefix):], "=")
   219  
   220  			// if the expected format is not present, then we skip getting the root partition
   221  			if len(rootSpec) < 2 {
   222  				if strings.HasPrefix(rootSpec[0], "/dev") {
   223  					return rootSpec[0], nil
   224  				}
   225  				return "", ErrCouldNotFindRootDevice
   226  			}
   227  
   228  			identifierType := strings.ToLower(rootSpec[0])
   229  			identifier := rootSpec[1]
   230  
   231  			return fmt.Sprintf("/dev/disk/by-%s/%s", identifierType, identifier), nil
   232  		}
   233  	}
   234  
   235  	return "", ErrCouldNotFindRootDevice
   236  }
   237  
   238  // getParentBlockDevice returns the parent blockdevice of a given blockdevice by parsing the syspath
   239  //
   240  // syspath looks like - /sys/devices/pci0000:00/0000:00:1f.2/ata1/host0/target0:0:0/0:0:0:0/block/sda/sda4
   241  // parent disk is present after block then partition of that disk
   242  //
   243  // for blockdevices that belong to the nvme subsystem, the symlink has different formats
   244  //     /sys/devices/pci0000:00/0000:00:0e.0/nvme/nvme0/nvme0n1/nvme0n1p1.
   245  //     /sys/devices/virtual/nvme-subsystem/nvme-subsys0/nvme0n1/nvme0n1p1
   246  // So we search for the "nvme" or "nvme-subsystem" subsystem instead of `block`. The
   247  // blockdevice will be available after the NVMe instance,
   248  //     nvme/instance/namespace
   249  //     nvme-subsystem/instance/namespace
   250  // The namespace will be the blockdevice.
   251  func getParentBlockDevice(sysPath string) (string, bool) {
   252  	blockSubsystem := "block"
   253  	nvmeSubsystem := "nvme"
   254  	nvmeSubsysClass := "nvme-subsystem"
   255  	parts := strings.Split(sysPath, "/")
   256  
   257  	// checking for block subsystem, return the next part after subsystem only
   258  	// if the length is greater. This check is to avoid an index out of range panic.
   259  	for i, part := range parts {
   260  		if part == blockSubsystem &&
   261  			len(parts)-1 >= i+1 {
   262  			return parts[i+1], true
   263  		}
   264  	}
   265  
   266  	// checking for nvme subsystem, return the 2nd item in hierarchy, which will be the
   267  	// nvme namespace. Length checking is to avoid index out of range in case of malformed
   268  	// links (extremely rare case)
   269  	for i, part := range parts {
   270  		if (part == nvmeSubsystem || part == nvmeSubsysClass) &&
   271  			len(parts)-1 >= i+2 {
   272  			return parts[i+2], true
   273  		}
   274  	}
   275  	return "", false
   276  }
   277  
   278  // getPartitionName gets the partition name from the mountpoint. Each line of a mounts file
   279  // is passed to the function, which is parsed and partition name is obtained
   280  // A mountLine contains data in the order:
   281  // 		device  mountpoint  filesystem  mountoptions
   282  //		eg: /dev/sda4 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
   283  func (m *DiskMountUtil) getPartitionName(mountLine string) (DeviceMountAttr, bool) {
   284  	mountAttr := DeviceMountAttr{}
   285  	isValid := false
   286  	if len(mountLine) == 0 {
   287  		return mountAttr, isValid
   288  	}
   289  	// mountoptions are ignored. device-path and mountpoint is used
   290  	if parts := strings.Split(mountLine, " "); parts[1] == m.mountPoint {
   291  		mountAttr.DevPath = getDeviceName(parts[0])
   292  		isValid = true
   293  	}
   294  	return mountAttr, isValid
   295  }
   296  
   297  // getMountName gets the mountpoint, filesystem etc from the partition name. Each line of a mounts
   298  // file is passed to the function, which is parsed to get the information
   299  // A mountLine contains data in the order:
   300  // 		device  mountpoint  filesystem  mountoptions
   301  //		eg: /dev/sda4 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
   302  func (m *DiskMountUtil) getMountName(mountLine string) (DeviceMountAttr, bool) {
   303  	mountAttr := DeviceMountAttr{}
   304  	isValid := false
   305  	if len(mountLine) == 0 {
   306  		return mountAttr, isValid
   307  	}
   308  	// mountoptions are ignored. devicepath, mountpoint and filesystem is used
   309  	if parts := strings.Split(mountLine, " "); parts[0] == m.devPath {
   310  		mountAttr.MountPoint = []string{parts[1]}
   311  		mountAttr.FileSystem = parts[2]
   312  		isValid = true
   313  	}
   314  	return mountAttr, isValid
   315  }
   316  
   317  func getCmdlineFile() string {
   318  	if fileExists(hostProcCmdLine) {
   319  		return hostProcCmdLine
   320  	}
   321  	return procCmdLine
   322  }
   323  
   324  // getDeviceName gets the blockdevice special file name.
   325  // eg: sda, sdb
   326  // if a mapper device is specified the symlink will be evaluated and the
   327  // dm-X name will be returned
   328  func getDeviceName(devPath string) string {
   329  	var err error
   330  	var deviceName string
   331  
   332  	deviceName = devPath
   333  	// if the device is a dm device
   334  	if strings.HasPrefix(devPath, "/dev/mapper") {
   335  		deviceName, err = filepath.EvalSymlinks(devPath)
   336  		if err != nil {
   337  			return ""
   338  		}
   339  	}
   340  	return strings.Replace(deviceName, "/dev/", "", 1)
   341  }
   342  
   343  func fileExists(file string) bool {
   344  	_, err := os.Stat(file)
   345  	if err != nil && os.IsNotExist(err) {
   346  		return false
   347  	}
   348  	return true
   349  }
   350  
   351  // mergeDeviceMountAttrs merges the second mountattr into the first. The merge is
   352  // performed as follows:
   353  // 1. If the DevPath of the first mountattr is empty, then it is set to the DevPath of
   354  // the second mountattr
   355  // 2. If the FileSystem of the first mountattr is empty, it is set to the FileSystem of
   356  // the second mountattr provided that the DevPaths of both the mountattrs match
   357  // 3. The MountPoint(s) of the second mountattr are appended to first's only if the
   358  // DevPath and the FileSystem of both the mountattrs match and the FileSystem of first
   359  // mountattr is non-empty (!= "")
   360  func mergeDeviceMountAttrs(ma *DeviceMountAttr, mb *DeviceMountAttr) {
   361  	if ma.DevPath == "" {
   362  		ma.DevPath = mb.DevPath
   363  	}
   364  	if ma.DevPath != mb.DevPath {
   365  		return
   366  	}
   367  	if ma.FileSystem == "" {
   368  		ma.FileSystem = mb.FileSystem
   369  	}
   370  	if ma.FileSystem != "" && ma.FileSystem == mb.FileSystem {
   371  		ma.MountPoint = append(ma.MountPoint, mb.MountPoint...)
   372  	}
   373  }