298 lines
10 KiB
Go
298 lines
10 KiB
Go
// Use and distribution licensed under the Apache license version 2.
|
|
//
|
|
// See the COPYING file in the root project directory for full text.
|
|
//
|
|
|
|
package ghw
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"howett.net/plist"
|
|
)
|
|
|
|
type diskOrPartitionPlistNode struct {
|
|
Content string
|
|
DeviceIdentifier string
|
|
DiskUUID string
|
|
VolumeName string
|
|
VolumeUUID string
|
|
Size int64
|
|
MountPoint string
|
|
Partitions []diskOrPartitionPlistNode
|
|
APFSVolumes []diskOrPartitionPlistNode
|
|
}
|
|
|
|
type diskUtilListPlist struct {
|
|
AllDisks []string
|
|
AllDisksAndPartitions []diskOrPartitionPlistNode
|
|
VolumesFromDisks []string
|
|
WholeDisks []string
|
|
}
|
|
|
|
type diskUtilInfoPlist struct {
|
|
AESHardware bool // true
|
|
Bootable bool // true
|
|
BooterDeviceIdentifier string // disk1s2
|
|
BusProtocol string // PCI-Express
|
|
CanBeMadeBootable bool // false
|
|
CanBeMadeBootableRequiresDestroy bool // false
|
|
Content string // some-uuid-foo-bar
|
|
DeviceBlockSize int64 // 4096
|
|
DeviceIdentifier string // disk1s1
|
|
DeviceNode string // /dev/disk1s1
|
|
DeviceTreePath string // IODeviceTree:/PCI0@0/RP17@1B/ANS2@0/AppleANS2Controller
|
|
DiskUUID string // some-uuid-foo-bar
|
|
Ejectable bool // false
|
|
EjectableMediaAutomaticUnderSoftwareControl bool // false
|
|
EjectableOnly bool // false
|
|
FilesystemName string // APFS
|
|
FilesystemType string // apfs
|
|
FilesystemUserVisibleName string // APFS
|
|
FreeSpace int64 // 343975677952
|
|
GlobalPermissionsEnabled bool // true
|
|
IOKitSize int64 // 499963174912
|
|
IORegistryEntryName string // Macintosh HD
|
|
Internal bool // true
|
|
MediaName string //
|
|
MediaType string // Generic
|
|
MountPoint string // /
|
|
ParentWholeDisk string // disk1
|
|
PartitionMapPartition bool // false
|
|
RAIDMaster bool // false
|
|
RAIDSlice bool // false
|
|
RecoveryDeviceIdentifier string // disk1s3
|
|
Removable bool // false
|
|
RemovableMedia bool // false
|
|
RemovableMediaOrExternalDevice bool // false
|
|
SMARTStatus string // Verified
|
|
Size int64 // 499963174912
|
|
SolidState bool // true
|
|
SupportsGlobalPermissionsDisable bool // true
|
|
SystemImage bool // false
|
|
TotalSize int64 // 499963174912
|
|
VolumeAllocationBlockSize int64 // 4096
|
|
VolumeName string // Macintosh HD
|
|
VolumeSize int64 // 499963174912
|
|
VolumeUUID string // some-uuid-foo-bar
|
|
WholeDisk bool // false
|
|
Writable bool // true
|
|
WritableMedia bool // true
|
|
WritableVolume bool // true
|
|
// also has a SMARTDeviceSpecificKeysMayVaryNotGuaranteed dict with various info
|
|
// NOTE: VolumeUUID sometimes == DiskUUID, but not always. So far Content is always a different UUID.
|
|
}
|
|
|
|
type ioregPlist struct {
|
|
// there's a lot more than just this...
|
|
ModelNumber string `plist:"Model Number"`
|
|
SerialNumber string `plist:"Serial Number"`
|
|
VendorName string `plist:"Vendor Name"`
|
|
}
|
|
|
|
func (ctx *context) getDiskUtilListPlist() (*diskUtilListPlist, error) {
|
|
out, err := exec.Command("diskutil", "list", "-plist").Output()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "diskutil list failed")
|
|
}
|
|
|
|
var data diskUtilListPlist
|
|
if _, err := plist.Unmarshal(out, &data); err != nil {
|
|
return nil, errors.Wrap(err, "diskutil list plist unmarshal failed")
|
|
}
|
|
|
|
return &data, nil
|
|
}
|
|
|
|
func (ctx *context) getDiskUtilInfoPlist(device string) (*diskUtilInfoPlist, error) {
|
|
out, err := exec.Command("diskutil", "info", "-plist", device).Output()
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "diskutil info for %q failed", device)
|
|
}
|
|
|
|
var data diskUtilInfoPlist
|
|
if _, err := plist.Unmarshal(out, &data); err != nil {
|
|
return nil, errors.Wrapf(err, "diskutil info plist unmarshal for %q failed", device)
|
|
}
|
|
|
|
return &data, nil
|
|
}
|
|
|
|
func (ctx *context) getIoregPlist(ioDeviceTreePath string) (*ioregPlist, error) {
|
|
name := path.Base(ioDeviceTreePath)
|
|
|
|
args := []string{
|
|
"ioreg",
|
|
"-a", // use XML output
|
|
"-d", "1", // limit device tree output depth to root node
|
|
"-r", // root device tree at matched node
|
|
"-n", name, // match by name
|
|
}
|
|
out, err := exec.Command(args[0], args[1:]...).Output()
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "ioreg query for %q failed", ioDeviceTreePath)
|
|
}
|
|
if out == nil || len(out) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var data []ioregPlist
|
|
if _, err := plist.Unmarshal(out, &data); err != nil {
|
|
return nil, errors.Wrapf(err, "ioreg unmarshal for %q failed", ioDeviceTreePath)
|
|
}
|
|
if len(data) != 1 {
|
|
err := errors.Errorf("ioreg unmarshal resulted in %d I/O device tree nodes (expected 1)", len(data))
|
|
return nil, err
|
|
}
|
|
|
|
return &data[0], nil
|
|
}
|
|
|
|
func (ctx *context) makePartition(disk, s diskOrPartitionPlistNode, isAPFS bool) (*Partition, error) {
|
|
if s.Size < 0 {
|
|
return nil, errors.Errorf("invalid size %q of partition %q", s.Size, s.DeviceIdentifier)
|
|
}
|
|
|
|
var partType string
|
|
if isAPFS {
|
|
partType = "APFS Volume"
|
|
} else {
|
|
partType = s.Content
|
|
}
|
|
|
|
info, err := ctx.getDiskUtilInfoPlist(s.DeviceIdentifier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Partition{
|
|
Disk: nil, // filled in later
|
|
Name: s.DeviceIdentifier,
|
|
Label: s.VolumeName,
|
|
MountPoint: s.MountPoint,
|
|
SizeBytes: uint64(s.Size),
|
|
Type: partType,
|
|
IsReadOnly: !info.WritableVolume,
|
|
}, nil
|
|
}
|
|
|
|
// driveTypeFromPlist looks at the supplied property list struct and attempts to
|
|
// determine the disk type
|
|
func (ctx *context) driveTypeFromPlist(
|
|
infoPlist *diskUtilInfoPlist,
|
|
) DriveType {
|
|
dt := DRIVE_TYPE_HDD
|
|
if infoPlist.SolidState {
|
|
dt = DRIVE_TYPE_SSD
|
|
}
|
|
// TODO(jaypipes): Figure out how to determine floppy and/or CD/optical
|
|
// drive type on Mac
|
|
return dt
|
|
}
|
|
|
|
// storageControllerFromPlist looks at the supplied property list struct and
|
|
// attempts to determine the storage controller in use for the device
|
|
func (ctx *context) storageControllerFromPlist(
|
|
infoPlist *diskUtilInfoPlist,
|
|
) StorageController {
|
|
sc := STORAGE_CONTROLLER_SCSI
|
|
if strings.HasSuffix(infoPlist.DeviceTreePath, "IONVMeController") {
|
|
sc = STORAGE_CONTROLLER_NVME
|
|
}
|
|
// TODO(jaypipes): I don't know if Mac even supports IDE controllers and
|
|
// the "virtio" controller is libvirt-specific
|
|
return sc
|
|
}
|
|
|
|
// busTypeFromPlist looks at the supplied property list struct and attempts to
|
|
// determine the bus type in use for the device
|
|
func (ctx *context) busTypeFromPlist(
|
|
infoPlist *diskUtilInfoPlist,
|
|
) BusType {
|
|
// TODO(jaypipes): Find out if Macs support any bus other than
|
|
// PCIe... it doesn't seem like they do
|
|
return BUS_TYPE_PCI
|
|
}
|
|
|
|
func (ctx *context) blockFillInfo(info *BlockInfo) error {
|
|
listPlist, err := ctx.getDiskUtilListPlist()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
return err
|
|
}
|
|
|
|
info.TotalPhysicalBytes = 0
|
|
info.Disks = make([]*Disk, 0, len(listPlist.AllDisksAndPartitions))
|
|
info.Partitions = []*Partition{}
|
|
|
|
for _, disk := range listPlist.AllDisksAndPartitions {
|
|
if disk.Size < 0 {
|
|
return errors.Errorf("invalid size %q of disk %q", disk.Size, disk.DeviceIdentifier)
|
|
}
|
|
|
|
infoPlist, err := ctx.getDiskUtilInfoPlist(disk.DeviceIdentifier)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if infoPlist.DeviceBlockSize < 0 {
|
|
return errors.Errorf("invalid block size %q of disk %q", infoPlist.DeviceBlockSize, disk.DeviceIdentifier)
|
|
}
|
|
|
|
busPath := strings.TrimPrefix(infoPlist.DeviceTreePath, "IODeviceTree:")
|
|
|
|
ioregPlist, err := ctx.getIoregPlist(infoPlist.DeviceTreePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ioregPlist == nil {
|
|
continue
|
|
}
|
|
|
|
// The NUMA node & WWN don't seem to be reported by any tools available by default in macOS.
|
|
diskReport := &Disk{
|
|
Name: disk.DeviceIdentifier,
|
|
SizeBytes: uint64(disk.Size),
|
|
PhysicalBlockSizeBytes: uint64(infoPlist.DeviceBlockSize),
|
|
DriveType: ctx.driveTypeFromPlist(infoPlist),
|
|
IsRemovable: infoPlist.Removable,
|
|
StorageController: ctx.storageControllerFromPlist(infoPlist),
|
|
BusType: ctx.busTypeFromPlist(infoPlist),
|
|
BusPath: busPath,
|
|
NUMANodeID: -1,
|
|
Vendor: ioregPlist.VendorName,
|
|
Model: ioregPlist.ModelNumber,
|
|
SerialNumber: ioregPlist.SerialNumber,
|
|
WWN: "",
|
|
Partitions: make([]*Partition, 0, len(disk.Partitions)+len(disk.APFSVolumes)),
|
|
}
|
|
|
|
for _, partition := range disk.Partitions {
|
|
part, err := ctx.makePartition(disk, partition, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
part.Disk = diskReport
|
|
diskReport.Partitions = append(diskReport.Partitions, part)
|
|
}
|
|
for _, volume := range disk.APFSVolumes {
|
|
part, err := ctx.makePartition(disk, volume, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
part.Disk = diskReport
|
|
diskReport.Partitions = append(diskReport.Partitions, part)
|
|
}
|
|
|
|
info.TotalPhysicalBytes += uint64(disk.Size)
|
|
info.Disks = append(info.Disks, diskReport)
|
|
info.Partitions = append(info.Partitions, diskReport.Partitions...)
|
|
}
|
|
|
|
return nil
|
|
}
|