github.com/jaypipes/ghw@v0.21.1/pkg/block/block_darwin.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 "fmt" 10 "os" 11 "os/exec" 12 "path" 13 "strings" 14 15 "github.com/pkg/errors" 16 "howett.net/plist" 17 ) 18 19 type diskOrPartitionPlistNode struct { 20 Content string 21 DeviceIdentifier string 22 DiskUUID string 23 VolumeName string 24 VolumeUUID string 25 Size int64 26 MountPoint string 27 Partitions []diskOrPartitionPlistNode 28 APFSVolumes []diskOrPartitionPlistNode 29 } 30 31 type diskUtilListPlist struct { 32 AllDisks []string 33 AllDisksAndPartitions []diskOrPartitionPlistNode 34 VolumesFromDisks []string 35 WholeDisks []string 36 } 37 38 type diskUtilInfoPlist struct { 39 AESHardware bool // true 40 Bootable bool // true 41 BooterDeviceIdentifier string // disk1s2 42 BusProtocol string // PCI-Express 43 CanBeMadeBootable bool // false 44 CanBeMadeBootableRequiresDestroy bool // false 45 Content string // some-uuid-foo-bar 46 DeviceBlockSize int64 // 4096 47 DeviceIdentifier string // disk1s1 48 DeviceNode string // /dev/disk1s1 49 DeviceTreePath string // IODeviceTree:/PCI0@0/RP17@1B/ANS2@0/AppleANS2Controller 50 DiskUUID string // some-uuid-foo-bar 51 Ejectable bool // false 52 EjectableMediaAutomaticUnderSoftwareControl bool // false 53 EjectableOnly bool // false 54 FilesystemName string // APFS 55 FilesystemType string // apfs 56 FilesystemUserVisibleName string // APFS 57 FreeSpace int64 // 343975677952 58 GlobalPermissionsEnabled bool // true 59 IOKitSize int64 // 499963174912 60 IORegistryEntryName string // Macintosh HD 61 Internal bool // true 62 MediaName string // 63 MediaType string // Generic 64 MountPoint string // / 65 ParentWholeDisk string // disk1 66 PartitionMapPartition bool // false 67 RAIDMaster bool // false 68 RAIDSlice bool // false 69 RecoveryDeviceIdentifier string // disk1s3 70 Removable bool // false 71 RemovableMedia bool // false 72 RemovableMediaOrExternalDevice bool // false 73 SMARTStatus string // Verified 74 Size int64 // 499963174912 75 SolidState bool // true 76 SupportsGlobalPermissionsDisable bool // true 77 SystemImage bool // false 78 TotalSize int64 // 499963174912 79 VolumeAllocationBlockSize int64 // 4096 80 VolumeName string // Macintosh HD 81 VolumeSize int64 // 499963174912 82 VolumeUUID string // some-uuid-foo-bar 83 WholeDisk bool // false 84 Writable bool // true 85 WritableMedia bool // true 86 WritableVolume bool // true 87 // also has a SMARTDeviceSpecificKeysMayVaryNotGuaranteed dict with various info 88 // NOTE: VolumeUUID sometimes == DiskUUID, but not always. So far Content is always a different UUID. 89 } 90 91 type ioregPlist struct { 92 // there's a lot more than just this... 93 ModelNumber string `plist:"Model Number"` 94 SerialNumber string `plist:"Serial Number"` 95 VendorName string `plist:"Vendor Name"` 96 } 97 98 func getDiskUtilListPlist() (*diskUtilListPlist, error) { 99 out, err := exec.Command("diskutil", "list", "-plist").Output() 100 if err != nil { 101 return nil, errors.Wrap(err, "diskutil list failed") 102 } 103 104 var data diskUtilListPlist 105 if _, err := plist.Unmarshal(out, &data); err != nil { 106 return nil, errors.Wrap(err, "diskutil list plist unmarshal failed") 107 } 108 109 return &data, nil 110 } 111 112 func getDiskUtilInfoPlist(device string) (*diskUtilInfoPlist, error) { 113 out, err := exec.Command("diskutil", "info", "-plist", device).Output() 114 if err != nil { 115 return nil, errors.Wrapf(err, "diskutil info for %q failed", device) 116 } 117 118 var data diskUtilInfoPlist 119 if _, err := plist.Unmarshal(out, &data); err != nil { 120 return nil, errors.Wrapf(err, "diskutil info plist unmarshal for %q failed", device) 121 } 122 123 return &data, nil 124 } 125 126 func getIoregPlist(ioDeviceTreePath string) (*ioregPlist, error) { 127 name := path.Base(ioDeviceTreePath) 128 129 args := []string{ 130 "ioreg", 131 "-a", // use XML output 132 "-d", "1", // limit device tree output depth to root node 133 "-r", // root device tree at matched node 134 "-n", name, // match by name 135 } 136 out, err := exec.Command(args[0], args[1:]...).Output() 137 if err != nil { 138 return nil, errors.Wrapf(err, "ioreg query for %q failed", ioDeviceTreePath) 139 } 140 if out == nil || len(out) == 0 { 141 return nil, nil 142 } 143 144 var data []ioregPlist 145 if _, err := plist.Unmarshal(out, &data); err != nil { 146 return nil, errors.Wrapf(err, "ioreg unmarshal for %q failed", ioDeviceTreePath) 147 } 148 if len(data) != 1 { 149 err := errors.Errorf("ioreg unmarshal resulted in %d I/O device tree nodes (expected 1)", len(data)) 150 return nil, err 151 } 152 153 return &data[0], nil 154 } 155 156 func makePartition(disk, s diskOrPartitionPlistNode, isAPFS bool) (*Partition, error) { 157 if s.Size < 0 { 158 return nil, errors.Errorf("invalid size %q of partition %q", s.Size, s.DeviceIdentifier) 159 } 160 161 var partType string 162 if isAPFS { 163 partType = "APFS Volume" 164 } else { 165 partType = s.Content 166 } 167 168 info, err := getDiskUtilInfoPlist(s.DeviceIdentifier) 169 if err != nil { 170 return nil, err 171 } 172 173 return &Partition{ 174 Disk: nil, // filled in later 175 Name: s.DeviceIdentifier, 176 Label: s.VolumeName, 177 MountPoint: s.MountPoint, 178 SizeBytes: uint64(s.Size), 179 Type: partType, 180 IsReadOnly: !info.WritableVolume, 181 UUID: s.VolumeUUID, 182 }, nil 183 } 184 185 // driveTypeFromPlist looks at the supplied property list struct and attempts to 186 // determine the disk type 187 func driveTypeFromPlist(infoPlist *diskUtilInfoPlist) DriveType { 188 dt := DriveTypeHDD 189 if infoPlist.SolidState { 190 dt = DriveTypeSSD 191 } 192 // TODO(jaypipes): Figure out how to determine floppy and/or CD/optical 193 // drive type on Mac 194 return dt 195 } 196 197 // storageControllerFromPlist looks at the supplied property list struct and 198 // attempts to determine the storage controller in use for the device 199 func storageControllerFromPlist(infoPlist *diskUtilInfoPlist) StorageController { 200 sc := StorageControllerSCSI 201 if strings.HasSuffix(infoPlist.DeviceTreePath, "IONVMeController") { 202 sc = StorageControllerNVMe 203 } 204 // TODO(jaypipes): I don't know if Mac even supports IDE controllers and 205 // the "virtio" controller is libvirt-specific 206 return sc 207 } 208 209 func (info *Info) load() error { 210 if !info.ctx.EnableTools { 211 return fmt.Errorf("EnableTools=false on darwin disables block support entirely.") 212 } 213 214 listPlist, err := getDiskUtilListPlist() 215 if err != nil { 216 fmt.Fprintln(os.Stderr, err.Error()) 217 return err 218 } 219 220 var tsb uint64 221 info.Disks = make([]*Disk, 0, len(listPlist.AllDisksAndPartitions)) 222 info.Partitions = []*Partition{} 223 224 for _, disk := range listPlist.AllDisksAndPartitions { 225 if disk.Size < 0 { 226 return errors.Errorf("invalid size %q of disk %q", disk.Size, disk.DeviceIdentifier) 227 } 228 229 infoPlist, err := getDiskUtilInfoPlist(disk.DeviceIdentifier) 230 if err != nil { 231 return err 232 } 233 if infoPlist.DeviceBlockSize < 0 { 234 return errors.Errorf("invalid block size %q of disk %q", infoPlist.DeviceBlockSize, disk.DeviceIdentifier) 235 } 236 237 busPath := strings.TrimPrefix(infoPlist.DeviceTreePath, "IODeviceTree:") 238 239 ioregPlist, err := getIoregPlist(infoPlist.DeviceTreePath) 240 if err != nil { 241 return err 242 } 243 if ioregPlist == nil { 244 continue 245 } 246 247 // The NUMA node & WWN don't seem to be reported by any tools available by default in macOS. 248 diskReport := &Disk{ 249 Name: disk.DeviceIdentifier, 250 SizeBytes: uint64(disk.Size), 251 PhysicalBlockSizeBytes: uint64(infoPlist.DeviceBlockSize), 252 DriveType: driveTypeFromPlist(infoPlist), 253 IsRemovable: infoPlist.Removable, 254 StorageController: storageControllerFromPlist(infoPlist), 255 BusPath: busPath, 256 NUMANodeID: -1, 257 Vendor: ioregPlist.VendorName, 258 Model: ioregPlist.ModelNumber, 259 SerialNumber: ioregPlist.SerialNumber, 260 WWN: "", 261 WWNNoExtension: "", 262 Partitions: make([]*Partition, 0, len(disk.Partitions)+len(disk.APFSVolumes)), 263 } 264 265 for _, partition := range disk.Partitions { 266 part, err := makePartition(disk, partition, false) 267 if err != nil { 268 return err 269 } 270 part.Disk = diskReport 271 diskReport.Partitions = append(diskReport.Partitions, part) 272 } 273 for _, volume := range disk.APFSVolumes { 274 part, err := makePartition(disk, volume, true) 275 if err != nil { 276 return err 277 } 278 part.Disk = diskReport 279 diskReport.Partitions = append(diskReport.Partitions, part) 280 } 281 282 tsb += uint64(disk.Size) 283 info.Disks = append(info.Disks, diskReport) 284 info.Partitions = append(info.Partitions, diskReport.Partitions...) 285 } 286 info.TotalSizeBytes = tsb 287 info.TotalPhysicalBytes = tsb 288 289 return nil 290 }