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  }