github.com/jaypipes/ghw@v0.21.1/pkg/block/block_linux.go (about) 1 // Use and distribution licensed under the Apache license version 2. 2 // 3 // See the COPYING file in the root project directory for full text. 4 // 5 6 package block 7 8 import ( 9 "bufio" 10 "io" 11 "os" 12 "path/filepath" 13 "strconv" 14 "strings" 15 16 "github.com/jaypipes/ghw/pkg/context" 17 "github.com/jaypipes/ghw/pkg/linuxpath" 18 "github.com/jaypipes/ghw/pkg/util" 19 ) 20 21 const ( 22 sectorSize = 512 23 ) 24 25 func (i *Info) load() error { 26 paths := linuxpath.New(i.ctx) 27 i.Disks = disks(i.ctx, paths) 28 var tsb uint64 29 for _, d := range i.Disks { 30 tsb += d.SizeBytes 31 } 32 i.TotalSizeBytes = tsb 33 i.TotalPhysicalBytes = tsb 34 return nil 35 } 36 37 func diskPhysicalBlockSizeBytes(paths *linuxpath.Paths, disk string) uint64 { 38 // We can find the sector size in Linux by looking at the 39 // /sys/block/$DEVICE/queue/physical_block_size file in sysfs 40 path := filepath.Join(paths.SysBlock, disk, "queue", "physical_block_size") 41 contents, err := os.ReadFile(path) 42 if err != nil { 43 return 0 44 } 45 size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64) 46 if err != nil { 47 return 0 48 } 49 return size 50 } 51 52 func diskSizeBytes(paths *linuxpath.Paths, disk string) uint64 { 53 // We can find the number of 512-byte sectors by examining the contents of 54 // /sys/block/$DEVICE/size and calculate the physical bytes accordingly. 55 path := filepath.Join(paths.SysBlock, disk, "size") 56 contents, err := os.ReadFile(path) 57 if err != nil { 58 return 0 59 } 60 size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64) 61 if err != nil { 62 return 0 63 } 64 return size * sectorSize 65 } 66 67 func diskNUMANodeID(paths *linuxpath.Paths, disk string) int { 68 link, err := os.Readlink(filepath.Join(paths.SysBlock, disk)) 69 if err != nil { 70 return -1 71 } 72 for partial := link; strings.HasPrefix(partial, "../devices/"); partial = filepath.Base(partial) { 73 if nodeContents, err := os.ReadFile(filepath.Join(paths.SysBlock, partial, "numa_node")); err != nil { 74 if nodeInt, err := strconv.Atoi(string(nodeContents)); err != nil { 75 return nodeInt 76 } 77 } 78 } 79 return -1 80 } 81 82 func diskVendor(paths *linuxpath.Paths, disk string) string { 83 // In Linux, the vendor for a disk device is found in the 84 // /sys/block/$DEVICE/device/vendor file in sysfs 85 path := filepath.Join(paths.SysBlock, disk, "device", "vendor") 86 contents, err := os.ReadFile(path) 87 if err != nil { 88 return util.UNKNOWN 89 } 90 return strings.TrimSpace(string(contents)) 91 } 92 93 // udevInfoDisk gets the udev info for a disk 94 func udevInfoDisk(paths *linuxpath.Paths, disk string) (map[string]string, error) { 95 // Get device major:minor numbers 96 devNo, err := os.ReadFile(filepath.Join(paths.SysBlock, disk, "dev")) 97 if err != nil { 98 return nil, err 99 } 100 return udevInfo(paths, string(devNo)) 101 } 102 103 // udevInfoPartition gets the udev info for a partition 104 func udevInfoPartition(paths *linuxpath.Paths, disk string, partition string) (map[string]string, error) { 105 // Get device major:minor numbers 106 devNo, err := os.ReadFile(filepath.Join(paths.SysBlock, disk, partition, "dev")) 107 if err != nil { 108 return nil, err 109 } 110 return udevInfo(paths, string(devNo)) 111 } 112 113 func udevInfo(paths *linuxpath.Paths, devNo string) (map[string]string, error) { 114 // Look up block device in udev runtime database 115 udevID := "b" + strings.TrimSpace(devNo) 116 udevBytes, err := os.ReadFile(filepath.Join(paths.RunUdevData, udevID)) 117 if err != nil { 118 return nil, err 119 } 120 121 udevInfo := make(map[string]string) 122 for _, udevLine := range strings.Split(string(udevBytes), "\n") { 123 if strings.HasPrefix(udevLine, "E:") { 124 if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 { 125 udevInfo[s[0]] = s[1] 126 } 127 } 128 } 129 return udevInfo, nil 130 } 131 132 func diskModel(paths *linuxpath.Paths, disk string) string { 133 info, err := udevInfoDisk(paths, disk) 134 if err != nil { 135 return util.UNKNOWN 136 } 137 138 if model, ok := info["ID_MODEL"]; ok { 139 return model 140 } 141 return util.UNKNOWN 142 } 143 144 func diskSerialNumber(paths *linuxpath.Paths, disk string) string { 145 info, err := udevInfoDisk(paths, disk) 146 if err != nil { 147 return util.UNKNOWN 148 } 149 150 // First try to use the serial from sg3_utils 151 if serial, ok := info["SCSI_IDENT_SERIAL"]; ok { 152 return serial 153 } 154 155 // Fall back to ID_SCSI_SERIAL 156 if serial, ok := info["ID_SCSI_SERIAL"]; ok { 157 return serial 158 } 159 160 // There are two serial number keys, ID_SERIAL and ID_SERIAL_SHORT The 161 // non-_SHORT version often duplicates vendor information collected 162 // elsewhere, so use _SHORT and fall back to ID_SERIAL if missing... 163 if serial, ok := info["ID_SERIAL_SHORT"]; ok { 164 return serial 165 } 166 if serial, ok := info["ID_SERIAL"]; ok { 167 return serial 168 } 169 return util.UNKNOWN 170 } 171 172 func diskBusPath(paths *linuxpath.Paths, disk string) string { 173 info, err := udevInfoDisk(paths, disk) 174 if err != nil { 175 return util.UNKNOWN 176 } 177 178 // There are two path keys, ID_PATH and ID_PATH_TAG. 179 // The difference seems to be _TAG has funky characters converted to underscores. 180 if path, ok := info["ID_PATH"]; ok { 181 return path 182 } 183 return util.UNKNOWN 184 } 185 186 func diskWWNNoExtension(paths *linuxpath.Paths, disk string) string { 187 info, err := udevInfoDisk(paths, disk) 188 if err != nil { 189 return util.UNKNOWN 190 } 191 192 if wwn, ok := info["ID_WWN"]; ok { 193 return wwn 194 } 195 return util.UNKNOWN 196 } 197 198 func diskWWN(paths *linuxpath.Paths, disk string) string { 199 info, err := udevInfoDisk(paths, disk) 200 if err != nil { 201 return util.UNKNOWN 202 } 203 204 // Trying ID_WWN_WITH_EXTENSION and falling back to ID_WWN is the same logic lsblk uses 205 if wwn, ok := info["ID_WWN_WITH_EXTENSION"]; ok { 206 return wwn 207 } 208 if wwn, ok := info["ID_WWN"]; ok { 209 return wwn 210 } 211 // Device Mapper devices get DM_WWN instead of ID_WWN_WITH_EXTENSION 212 if wwn, ok := info["DM_WWN"]; ok { 213 return wwn 214 } 215 return util.UNKNOWN 216 } 217 218 // diskPartitions takes the name of a disk (note: *not* the path of the disk, 219 // but just the name. In other words, "sda", not "/dev/sda" and "nvme0n1" not 220 // "/dev/nvme0n1") and returns a slice of pointers to Partition structs 221 // representing the partitions in that disk 222 func diskPartitions(ctx *context.Context, paths *linuxpath.Paths, disk string) []*Partition { 223 out := make([]*Partition, 0) 224 path := filepath.Join(paths.SysBlock, disk) 225 files, err := os.ReadDir(path) 226 if err != nil { 227 ctx.Warn("failed to read disk partitions: %s\n", err) 228 return out 229 } 230 for _, file := range files { 231 fname := file.Name() 232 if !strings.HasPrefix(fname, disk) { 233 continue 234 } 235 size := partitionSizeBytes(paths, disk, fname) 236 mp, pt, ro := partitionInfo(paths, fname) 237 du := diskPartUUID(paths, disk, fname) 238 label := diskPartLabel(paths, disk, fname) 239 if pt == "" { 240 pt = diskPartTypeUdev(paths, disk, fname) 241 } 242 fsLabel := diskFSLabel(paths, disk, fname) 243 p := &Partition{ 244 Name: fname, 245 SizeBytes: size, 246 MountPoint: mp, 247 Type: pt, 248 IsReadOnly: ro, 249 UUID: du, 250 Label: label, 251 FilesystemLabel: fsLabel, 252 } 253 out = append(out, p) 254 } 255 return out 256 } 257 258 func diskFSLabel(paths *linuxpath.Paths, disk string, partition string) string { 259 info, err := udevInfoPartition(paths, disk, partition) 260 if err != nil { 261 return util.UNKNOWN 262 } 263 264 if label, ok := info["ID_FS_LABEL"]; ok { 265 return label 266 } 267 return util.UNKNOWN 268 } 269 270 func diskPartLabel(paths *linuxpath.Paths, disk string, partition string) string { 271 info, err := udevInfoPartition(paths, disk, partition) 272 if err != nil { 273 return util.UNKNOWN 274 } 275 276 if label, ok := info["ID_PART_ENTRY_NAME"]; ok { 277 return label 278 } 279 return util.UNKNOWN 280 } 281 282 // diskPartTypeUdev gets the partition type from the udev database directly and its only used as fallback when 283 // the partition is not mounted, so we cannot get the type from paths.ProcMounts from the partitionInfo function 284 func diskPartTypeUdev(paths *linuxpath.Paths, disk string, partition string) string { 285 info, err := udevInfoPartition(paths, disk, partition) 286 if err != nil { 287 return util.UNKNOWN 288 } 289 290 if pType, ok := info["ID_FS_TYPE"]; ok { 291 return pType 292 } 293 return util.UNKNOWN 294 } 295 296 func diskPartUUID(paths *linuxpath.Paths, disk string, partition string) string { 297 info, err := udevInfoPartition(paths, disk, partition) 298 if err != nil { 299 return util.UNKNOWN 300 } 301 302 if pType, ok := info["ID_PART_ENTRY_UUID"]; ok { 303 return pType 304 } 305 return util.UNKNOWN 306 } 307 308 func diskIsRemovable(paths *linuxpath.Paths, disk string) bool { 309 path := filepath.Join(paths.SysBlock, disk, "removable") 310 contents, err := os.ReadFile(path) 311 if err != nil { 312 return false 313 } 314 removable := strings.TrimSpace(string(contents)) 315 return removable == "1" 316 } 317 318 func disks(ctx *context.Context, paths *linuxpath.Paths) []*Disk { 319 // In Linux, we could use the fdisk, lshw or blockdev commands to list disk 320 // information, however all of these utilities require root privileges to 321 // run. We can get all of this information by examining the /sys/block 322 // and /sys/class/block files 323 disks := make([]*Disk, 0) 324 files, err := os.ReadDir(paths.SysBlock) 325 if err != nil { 326 return nil 327 } 328 for _, file := range files { 329 dname := file.Name() 330 331 driveType, storageController := diskTypes(dname) 332 // TODO(jaypipes): Move this into diskTypes() once abstracting 333 // diskIsRotational for ease of unit testing 334 // Only reclassify HDD to SSD if non-rotational to avoid changing already correct types. 335 // This addresses changed kernel behavior where rotational detection may be unreliable, 336 // where some kernels report CD-ROM drives as non-rotational, incorrectly classifying them as SSD. 337 if !diskIsRotational(ctx, paths, dname) && driveType == DRIVE_TYPE_HDD { 338 driveType = DRIVE_TYPE_SSD 339 } 340 size := diskSizeBytes(paths, dname) 341 pbs := diskPhysicalBlockSizeBytes(paths, dname) 342 busPath := diskBusPath(paths, dname) 343 node := diskNUMANodeID(paths, dname) 344 vendor := diskVendor(paths, dname) 345 model := diskModel(paths, dname) 346 serialNo := diskSerialNumber(paths, dname) 347 wwn := diskWWN(paths, dname) 348 wwnNoExtension := diskWWNNoExtension(paths, dname) 349 removable := diskIsRemovable(paths, dname) 350 351 if storageController == STORAGE_CONTROLLER_LOOP && size == 0 { 352 // We don't care about unused loop devices... 353 continue 354 } 355 d := &Disk{ 356 Name: dname, 357 SizeBytes: size, 358 PhysicalBlockSizeBytes: pbs, 359 DriveType: driveType, 360 IsRemovable: removable, 361 StorageController: storageController, 362 BusPath: busPath, 363 NUMANodeID: node, 364 Vendor: vendor, 365 Model: model, 366 SerialNumber: serialNo, 367 WWN: wwn, 368 WWNNoExtension: wwnNoExtension, 369 } 370 371 parts := diskPartitions(ctx, paths, dname) 372 // Map this Disk object into the Partition... 373 for _, part := range parts { 374 part.Disk = d 375 } 376 d.Partitions = parts 377 378 disks = append(disks, d) 379 } 380 381 return disks 382 } 383 384 // diskTypes returns the drive type, storage controller and bus type of a disk 385 func diskTypes(dname string) ( 386 DriveType, 387 StorageController, 388 ) { 389 // The conditionals below which set the controller and drive type are 390 // based on information listed here: 391 // https://en.wikipedia.org/wiki/Device_file 392 driveType := DriveTypeUnknown 393 storageController := StorageControllerUnknown 394 if strings.HasPrefix(dname, "fd") { 395 driveType = DriveTypeFDD 396 } else if strings.HasPrefix(dname, "sd") { 397 driveType = DriveTypeHDD 398 storageController = StorageControllerSCSI 399 } else if strings.HasPrefix(dname, "hd") { 400 driveType = DriveTypeHDD 401 storageController = StorageControllerIDE 402 } else if strings.HasPrefix(dname, "vd") { 403 driveType = DriveTypeHDD 404 storageController = StorageControllerVirtIO 405 } else if strings.HasPrefix(dname, "nvme") { 406 driveType = DriveTypeSSD 407 storageController = StorageControllerNVMe 408 } else if strings.HasPrefix(dname, "sr") { 409 driveType = DriveTypeODD 410 storageController = StorageControllerSCSI 411 } else if strings.HasPrefix(dname, "xvd") { 412 driveType = DriveTypeHDD 413 storageController = StorageControllerSCSI 414 } else if strings.HasPrefix(dname, "mmc") { 415 driveType = DriveTypeSSD 416 storageController = StorageControllerMMC 417 } else if strings.HasPrefix(dname, "loop") { 418 driveType = DriveTypeVirtual 419 storageController = StorageControllerLoop 420 } 421 422 return driveType, storageController 423 } 424 425 func diskIsRotational(ctx *context.Context, paths *linuxpath.Paths, devName string) bool { 426 path := filepath.Join(paths.SysBlock, devName, "queue", "rotational") 427 contents := util.SafeIntFromFile(ctx, path) 428 return contents == 1 429 } 430 431 // partitionSizeBytes returns the size in bytes of the partition given a disk 432 // name and a partition name. Note: disk name and partition name do *not* 433 // contain any leading "/dev" parts. In other words, they are *names*, not 434 // paths. 435 func partitionSizeBytes(paths *linuxpath.Paths, disk string, part string) uint64 { 436 path := filepath.Join(paths.SysBlock, disk, part, "size") 437 contents, err := os.ReadFile(path) 438 if err != nil { 439 return 0 440 } 441 size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64) 442 if err != nil { 443 return 0 444 } 445 return size * sectorSize 446 } 447 448 // Given a full or short partition name, returns the mount point, the type of 449 // the partition and whether it's readonly 450 func partitionInfo(paths *linuxpath.Paths, part string) (string, string, bool) { 451 // Allow calling PartitionInfo with either the full partition name 452 // "/dev/sda1" or just "sda1" 453 if !strings.HasPrefix(part, "/dev") { 454 part = "/dev/" + part 455 } 456 457 // mount entries for mounted partitions look like this: 458 // /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0 459 var r io.ReadCloser 460 r, err := os.Open(paths.ProcMounts) 461 if err != nil { 462 return "", "", true 463 } 464 defer util.SafeClose(r) 465 466 scanner := bufio.NewScanner(r) 467 for scanner.Scan() { 468 line := scanner.Text() 469 entry := parseMountEntry(line) 470 if entry == nil || entry.Partition != part { 471 continue 472 } 473 ro := true 474 for _, opt := range entry.Options { 475 if opt == "rw" { 476 ro = false 477 break 478 } 479 } 480 481 return entry.Mountpoint, entry.FilesystemType, ro 482 } 483 return "", "", true 484 } 485 486 type mountEntry struct { 487 Partition string 488 Mountpoint string 489 FilesystemType string 490 Options []string 491 } 492 493 func parseMountEntry(line string) *mountEntry { 494 // mount entries for mounted partitions look like this: 495 // /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0 496 if line[0] != '/' { 497 return nil 498 } 499 fields := strings.Fields(line) 500 501 if len(fields) < 4 { 502 return nil 503 } 504 505 // We do some special parsing of the mountpoint, which may contain space, 506 // tab and newline characters, encoded into the mount entry line using their 507 // octal-to-string representations. From the GNU mtab man pages: 508 // 509 // "Therefore these characters are encoded in the files and the getmntent 510 // function takes care of the decoding while reading the entries back in. 511 // '\040' is used to encode a space character, '\011' to encode a tab 512 // character, '\012' to encode a newline character, and '\\' to encode a 513 // backslash." 514 mp := fields[1] 515 r := strings.NewReplacer( 516 "\\011", "\t", "\\012", "\n", "\\040", " ", "\\\\", "\\", 517 ) 518 mp = r.Replace(mp) 519 520 res := &mountEntry{ 521 Partition: fields[0], 522 Mountpoint: mp, 523 FilesystemType: fields[2], 524 } 525 opts := strings.Split(fields[3], ",") 526 res.Options = opts 527 return res 528 }