diff --git a/example/macOS/main.go b/example/macOS/main.go index aac5e68d..897702db 100644 --- a/example/macOS/main.go +++ b/example/macOS/main.go @@ -14,9 +14,11 @@ import ( ) var install bool +var nbdURL string func init() { flag.BoolVar(&install, "install", false, "run command as install mode") + flag.StringVar(&nbdURL, "nbd-url", "", "nbd url (e.g. nbd+unix:///export?socket=nbd.sock)") } func main() { @@ -142,21 +144,33 @@ func computeMemorySize() uint64 { } func createBlockDeviceConfiguration(diskPath string) (*vz.VirtioBlockDeviceConfiguration, error) { - // create disk image with 64 GiB - if err := vz.CreateDiskImage(diskPath, 64*1024*1024*1024); err != nil { - if !os.IsExist(err) { - return nil, fmt.Errorf("failed to create disk image: %w", err) + var attachment vz.StorageDeviceAttachment + var err error + + if nbdURL == "" { + // create disk image with 64 GiB + if err := vz.CreateDiskImage(diskPath, 64*1024*1024*1024); err != nil { + if !os.IsExist(err) { + return nil, fmt.Errorf("failed to create disk image: %w", err) + } } - } - diskImageAttachment, err := vz.NewDiskImageStorageDeviceAttachment( - diskPath, - false, - ) + attachment, err = vz.NewDiskImageStorageDeviceAttachment( + diskPath, + false, + ) + } else { + attachment, err = vz.NewNetworkBlockDeviceStorageDeviceAttachment( + nbdURL, + 10*time.Second, + false, + vz.DiskSynchronizationModeFull, + ) + } if err != nil { return nil, err } - return vz.NewVirtioBlockDeviceConfiguration(diskImageAttachment) + return vz.NewVirtioBlockDeviceConfiguration(attachment) } func createGraphicsDeviceConfiguration() (*vz.MacGraphicsDeviceConfiguration, error) { diff --git a/osversion_test.go b/osversion_test.go index dc4ae6a4..348fa077 100644 --- a/osversion_test.go +++ b/osversion_test.go @@ -319,6 +319,10 @@ func TestAvailableVersion(t *testing.T) { _, err := NewDiskBlockDeviceStorageDeviceAttachment(nil, false, DiskSynchronizationModeFull) return err }, + "NewNetworkBlockDeviceStorageDeviceAttachment": func() error { + _, err := NewNetworkBlockDeviceStorageDeviceAttachment("", 0, false, DiskSynchronizationModeFull) + return err + }, } for name, fn := range cases { t.Run(name, func(t *testing.T) { diff --git a/storage.go b/storage.go index 53be874f..f1c51c43 100644 --- a/storage.go +++ b/storage.go @@ -12,6 +12,7 @@ package vz import "C" import ( "os" + "time" "github.com/Code-Hex/vz/v3/internal/objc" ) @@ -397,3 +398,59 @@ func NewDiskBlockDeviceStorageDeviceAttachment(file *os.File, readOnly bool, syn }) return attachment, nil } + +// NetworkBlockDeviceStorageDeviceAttachment is a storage device attachment that is backed by a +// NBD (Network Block Device) server. +// +// Using this attachment requires the app to have the com.apple.security.network.client entitlement +// because this attachment opens an outgoing network connection. +// +// For more information about the NBD URL format read: +// https://github.com/NetworkBlockDevice/nbd/blob/master/doc/uri.md +type NetworkBlockDeviceStorageDeviceAttachment struct { + *pointer + + *baseStorageDeviceAttachment +} + +var _ StorageDeviceAttachment = (*NetworkBlockDeviceStorageDeviceAttachment)(nil) + +// NewNetworkBlockDeviceStorageDeviceAttachment creates a new network block device storage attachment from an NBD +// Uniform Resource Indicator (URI) represented as a URL, timeout value, and read-only and synchronization modes +// that you provide. +// +// - url is the NBD server URI. The format specified by https://github.com/NetworkBlockDevice/nbd/blob/master/doc/uri.md +// - timeout is the duration for the connection between the client and server. When the timeout expires, an attempt to reconnect with the server takes place. +// - forcedReadOnly if true forces the disk attachment to be read-only, regardless of whether or not the NBD server supports write requests. +// - syncMode is one of the available DiskSynchronizationMode options. +// +// This is only supported on macOS 14 and newer, error will +// be returned on older versions. +func NewNetworkBlockDeviceStorageDeviceAttachment(url string, timeout time.Duration, forcedReadOnly bool, syncMode DiskSynchronizationMode) (*NetworkBlockDeviceStorageDeviceAttachment, error) { + if err := macOSAvailable(14); err != nil { + return nil, err + } + + nserrPtr := newNSErrorAsNil() + + urlChar := charWithGoString(url) + defer urlChar.Free() + attachment := &NetworkBlockDeviceStorageDeviceAttachment{ + pointer: objc.NewPointer( + C.newVZNetworkBlockDeviceStorageDeviceAttachment( + urlChar.CString(), + C.double(timeout.Seconds()), + C.bool(forcedReadOnly), + C.int(syncMode), + &nserrPtr, + ), + ), + } + if err := newNSError(nserrPtr); err != nil { + return nil, err + } + objc.SetFinalizer(attachment, func(self *NetworkBlockDeviceStorageDeviceAttachment) { + objc.Release(self) + }) + return attachment, nil +} diff --git a/virtualization_14.h b/virtualization_14.h index 680bc952..c8abb273 100644 --- a/virtualization_14.h +++ b/virtualization_14.h @@ -15,4 +15,5 @@ /* macOS 14 API */ void *newVZNVMExpressControllerDeviceConfiguration(void *attachment); -void *newVZDiskBlockDeviceStorageDeviceAttachment(int fileDescriptor, bool readOnly, int syncMode, void **error); \ No newline at end of file +void *newVZDiskBlockDeviceStorageDeviceAttachment(int fileDescriptor, bool readOnly, int syncMode, void **error); +void *newVZNetworkBlockDeviceStorageDeviceAttachment(const char *url, double timeout, bool forcedReadOnly, int syncMode, void **error); \ No newline at end of file diff --git a/virtualization_14.m b/virtualization_14.m index da692e98..f528518a 100644 --- a/virtualization_14.m +++ b/virtualization_14.m @@ -50,4 +50,37 @@ } #endif RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +/*! + @abstract Initialize a network block device storage attachment from an NBD URI. + @param uri The NBD’s URI represented as a URL. + @param timeout The timeout value in seconds for the connection between the client and server. When the timeout expires, an attempt to reconnect with the server takes place. + @param forcedReadOnly If YES, the framework forces the disk attachment to be read-only, regardless of whether or not the NBD server supports write requests. + @param synchronizationMode Defines how the disk synchronizes with the underlying storage when the guest operating system flushes data. + @param error If not nil, assigned with the error if the initialization failed. + @return An initialized `VZDiskBlockDeviceStorageDeviceAttachment` or nil if there was an error. + @discussion + The forcedReadOnly parameter affects how framework exposes the NBD client to the guest operating + system by the storage controller. As part of the NBD protocol, the NBD server advertises whether + or not the disk exposed by the NBD client is read-only during the handshake phase of the protocol. + + Setting forcedReadOnly to YES forces the NBD client to show up as read-only to the guest + regardless of whether or not the NBD server advertises itself as read-only. + */ +void *newVZNetworkBlockDeviceStorageDeviceAttachment(const char *uri, double timeout, bool forcedReadOnly, int syncMode, void **error) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:uri]]; + + return [[VZNetworkBlockDeviceStorageDeviceAttachment alloc] + initWithURL:url + timeout:(NSTimeInterval)timeout + forcedReadOnly:(BOOL)forcedReadOnly + synchronizationMode:(VZDiskSynchronizationMode)syncMode + error:(NSError *_Nullable *_Nullable)error]; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); } \ No newline at end of file