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 }