11
// The VirtioDevice interface is an interface which is implemented by all virtio devices.
12
type VirtioDevice VMComponent
15
// Possible values for VirtioInput.InputType
16
VirtioInputPointingDevice = "pointing"
17
VirtioInputKeyboardDevice = "keyboard"
19
// Options for VirtioGPUResolution
20
VirtioGPUResolutionWidth = "width"
21
VirtioGPUResolutionHeight = "height"
23
// Default VirtioGPU Resolution
24
defaultVirtioGPUResolutionWidth = 800
25
defaultVirtioGPUResolutionHeight = 600
28
// VirtioInput configures an input device, such as a keyboard or pointing device
29
// (mouse) that the virtual machine can use
30
type VirtioInput struct {
31
InputType string `json:"inputType"` // currently supports "pointing" and "keyboard" input types
34
type VirtioGPUResolution struct {
35
Width int `json:"width"`
36
Height int `json:"height"`
39
// VirtioGPU configures a GPU device, such as the host computer's display
40
type VirtioGPU struct {
41
UsesGUI bool `json:"usesGUI"`
45
// VirtioVsock configures of a virtio-vsock device allowing 2-way communication
46
// between the host and the virtual machine type
47
type VirtioVsock struct {
48
// Port is the virtio-vsock port used for this device, see `man vsock` for more
51
// SocketURL is the path to a unix socket on the host to use for the virtio-vsock communication with the guest.
53
// If true, vsock connections will have to be done from guest to host. If false, vsock connections will only be possible
58
// VirtioBlk configures a disk device.
59
type VirtioBlk struct {
61
DeviceIdentifier string
64
type DirectorySharingConfig struct {
68
// VirtioFs configures directory sharing between the guest and the host.
70
DirectorySharingConfig
74
// RosettaShare configures rosetta support in the guest to run Intel binaries on Apple CPUs
75
type RosettaShare struct {
76
DirectorySharingConfig
80
// virtioRng configures a random number generator (RNG) device.
81
type VirtioRng struct {
84
// TODO: Add BridgedNetwork support
85
// https://github.com/Code-Hex/vz/blob/d70a0533bf8ed0fa9ab22fa4d4ca554b7c3f3ce5/network.go#L81-L82
87
// VirtioNet configures the virtual machine networking.
88
type VirtioNet struct {
90
MacAddress net.HardwareAddr
91
// file parameter is holding a connected datagram socket.
92
// see https://github.com/Code-Hex/vz/blob/7f648b6fb9205d6f11792263d79876e3042c33ec/network.go#L113-L155
98
// VirtioSerial configures the virtual machine serial ports.
99
type VirtioSerial struct {
104
// TODO: Add VirtioBalloon
105
// https://github.com/Code-Hex/vz/blob/master/memory_balloon.go
112
func strToOption(str string) option {
113
splitStr := strings.SplitN(str, "=", 2)
118
if len(splitStr) > 1 {
119
opt.value = splitStr[1]
125
func strvToOptions(opts []string) []option {
126
parsedOpts := []option{}
127
for _, opt := range opts {
131
parsedOpts = append(parsedOpts, strToOption(opt))
137
func deviceFromCmdLine(deviceOpts string) (VirtioDevice, error) {
138
opts := strings.Split(deviceOpts, ",")
140
return nil, fmt.Errorf("empty option list in command line argument")
145
dev = &RosettaShare{}
147
dev = virtioBlkNewEmpty()
154
case "virtio-serial":
155
dev = &VirtioSerial{}
158
case "usb-mass-storage":
159
dev = usbMassStorageNewEmpty()
165
return nil, fmt.Errorf("unknown device type: %s", opts[0])
168
parsedOpts := strvToOptions(opts[1:])
169
if err := dev.FromOptions(parsedOpts); err != nil {
176
// VirtioSerialNew creates a new serial device for the virtual machine. The
177
// output the virtual machine sent to the serial port will be written to the
178
// file at logFilePath.
179
func VirtioSerialNew(logFilePath string) (VirtioDevice, error) {
180
return &VirtioSerial{
181
LogFile: logFilePath,
185
func VirtioSerialNewStdio() (VirtioDevice, error) {
186
return &VirtioSerial{
191
func (dev *VirtioSerial) validate() error {
192
if dev.LogFile != "" && dev.UsesStdio {
193
return fmt.Errorf("'logFilePath' and 'stdio' cannot be set at the same time")
195
if dev.LogFile == "" && !dev.UsesStdio {
196
return fmt.Errorf("one of 'logFilePath' or 'stdio' must be set")
202
func (dev *VirtioSerial) ToCmdLine() ([]string, error) {
203
if err := dev.validate(); err != nil {
207
return []string{"--device", "virtio-serial,stdio"}, nil
210
return []string{"--device", fmt.Sprintf("virtio-serial,logFilePath=%s", dev.LogFile)}, nil
213
func (dev *VirtioSerial) FromOptions(options []option) error {
214
for _, option := range options {
217
dev.LogFile = option.value
219
if option.value != "" {
220
return fmt.Errorf("unexpected value for virtio-serial 'stdio' option: %s", option.value)
224
return fmt.Errorf("unknown option for virtio-serial devices: %s", option.key)
228
return dev.validate()
231
// VirtioInputNew creates a new input device for the virtual machine.
232
// The inputType parameter is the type of virtio-input device that will be added
234
func VirtioInputNew(inputType string) (VirtioDevice, error) {
236
InputType: inputType,
238
if err := dev.validate(); err != nil {
245
func (dev *VirtioInput) validate() error {
246
if dev.InputType != VirtioInputPointingDevice && dev.InputType != VirtioInputKeyboardDevice {
247
return fmt.Errorf("unknown option for virtio-input devices: %s", dev.InputType)
253
func (dev *VirtioInput) ToCmdLine() ([]string, error) {
254
if err := dev.validate(); err != nil {
258
return []string{"--device", fmt.Sprintf("virtio-input,%s", dev.InputType)}, nil
261
func (dev *VirtioInput) FromOptions(options []option) error {
262
for _, option := range options {
264
case VirtioInputPointingDevice, VirtioInputKeyboardDevice:
265
if option.value != "" {
266
return fmt.Errorf(fmt.Sprintf("unexpected value for virtio-input %s option: %s", option.key, option.value))
268
dev.InputType = option.key
270
return fmt.Errorf("unknown option for virtio-input devices: %s", option.key)
273
return dev.validate()
276
// VirtioGPUNew creates a new gpu device for the virtual machine.
277
// The usesGUI parameter determines whether a graphical application window will
279
func VirtioGPUNew() (VirtioDevice, error) {
282
VirtioGPUResolution: VirtioGPUResolution{
283
Width: defaultVirtioGPUResolutionWidth,
284
Height: defaultVirtioGPUResolutionHeight,
289
func (dev *VirtioGPU) validate() error {
290
if dev.Height < 1 || dev.Width < 1 {
291
return fmt.Errorf("invalid dimensions for virtio-gpu device resolution: %dx%d", dev.Width, dev.Height)
297
func (dev *VirtioGPU) ToCmdLine() ([]string, error) {
298
if err := dev.validate(); err != nil {
302
return []string{"--device", fmt.Sprintf("virtio-gpu,width=%d,height=%d", dev.Width, dev.Height)}, nil
305
func (dev *VirtioGPU) FromOptions(options []option) error {
306
for _, option := range options {
308
case VirtioGPUResolutionHeight:
309
height, err := strconv.Atoi(option.value)
310
if err != nil || height < 1 {
311
return fmt.Errorf(fmt.Sprintf("Invalid value for virtio-gpu %s: %s", option.key, option.value))
315
case VirtioGPUResolutionWidth:
316
width, err := strconv.Atoi(option.value)
317
if err != nil || width < 1 {
318
return fmt.Errorf(fmt.Sprintf("Invalid value for virtio-gpu %s: %s", option.key, option.value))
323
return fmt.Errorf("unknown option for virtio-gpu devices: %s", option.key)
327
if dev.Width == 0 && dev.Height == 0 {
328
dev.Width = defaultVirtioGPUResolutionWidth
329
dev.Height = defaultVirtioGPUResolutionHeight
332
return dev.validate()
335
// VirtioNetNew creates a new network device for the virtual machine. It will
336
// use macAddress as its MAC address.
337
func VirtioNetNew(macAddress string) (*VirtioNet, error) {
338
var hwAddr net.HardwareAddr
340
if macAddress != "" {
342
if hwAddr, err = net.ParseMAC(macAddress); err != nil {
352
// Set the socket to use for the network communication
354
// This maps the virtual machine network interface to a connected datagram
355
// socket. This means all network traffic on this interface will go through
357
// file must be a connected datagram (SOCK_DGRAM) socket.
358
func (dev *VirtioNet) SetSocket(file *os.File) {
363
func (dev *VirtioNet) SetUnixSocketPath(path string) {
364
dev.UnixSocketPath = path
368
func (dev *VirtioNet) validate() error {
369
if dev.Nat && dev.Socket != nil {
370
return fmt.Errorf("'nat' and 'fd' cannot be set at the same time")
372
if dev.Nat && dev.UnixSocketPath != "" {
373
return fmt.Errorf("'nat' and 'unixSocketPath' cannot be set at the same time")
375
if dev.Socket != nil && dev.UnixSocketPath != "" {
376
return fmt.Errorf("'fd' and 'unixSocketPath' cannot be set at the same time")
378
if !dev.Nat && dev.Socket == nil && dev.UnixSocketPath == "" {
379
return fmt.Errorf("one of 'nat' or 'fd' or 'unixSocketPath' must be set")
385
func (dev *VirtioNet) ToCmdLine() ([]string, error) {
386
if err := dev.validate(); err != nil {
390
builder := strings.Builder{}
391
builder.WriteString("virtio-net")
394
builder.WriteString(",nat")
395
case dev.UnixSocketPath != "":
396
fmt.Fprintf(&builder, ",unixSocketPath=%s", dev.UnixSocketPath)
398
fmt.Fprintf(&builder, ",fd=%d", dev.Socket.Fd())
401
if len(dev.MacAddress) != 0 {
402
builder.WriteString(fmt.Sprintf(",mac=%s", dev.MacAddress))
405
return []string{"--device", builder.String()}, nil
408
func (dev *VirtioNet) FromOptions(options []option) error {
409
for _, option := range options {
412
if option.value != "" {
413
return fmt.Errorf("unexpected value for virtio-net 'nat' option: %s", option.value)
417
macAddress, err := net.ParseMAC(option.value)
421
dev.MacAddress = macAddress
423
fd, err := strconv.Atoi(option.value)
427
dev.Socket = os.NewFile(uintptr(fd), "vfkit virtio-net socket")
428
case "unixSocketPath":
429
dev.UnixSocketPath = option.value
431
return fmt.Errorf("unknown option for virtio-net devices: %s", option.key)
435
return dev.validate()
438
// VirtioRngNew creates a new random number generator device to feed entropy
439
// into the virtual machine.
440
func VirtioRngNew() (VirtioDevice, error) {
441
return &VirtioRng{}, nil
444
func (dev *VirtioRng) ToCmdLine() ([]string, error) {
445
return []string{"--device", "virtio-rng"}, nil
448
func (dev *VirtioRng) FromOptions(options []option) error {
449
if len(options) != 0 {
450
return fmt.Errorf("unknown options for virtio-rng devices: %s", options)
455
func virtioBlkNewEmpty() *VirtioBlk {
457
StorageConfig: StorageConfig{
458
DevName: "virtio-blk",
460
DeviceIdentifier: "",
464
// VirtioBlkNew creates a new disk to use in the virtual machine. It will use
465
// the file at imagePath as the disk image. This image must be in raw format.
466
func VirtioBlkNew(imagePath string) (*VirtioBlk, error) {
467
virtioBlk := virtioBlkNewEmpty()
468
virtioBlk.ImagePath = imagePath
470
return virtioBlk, nil
473
func (dev *VirtioBlk) SetDeviceIdentifier(devID string) {
474
dev.DeviceIdentifier = devID
477
func (dev *VirtioBlk) FromOptions(options []option) error {
478
unhandledOpts := []option{}
479
for _, option := range options {
482
dev.DeviceIdentifier = option.value
484
unhandledOpts = append(unhandledOpts, option)
488
return dev.StorageConfig.FromOptions(unhandledOpts)
491
func (dev *VirtioBlk) ToCmdLine() ([]string, error) {
492
cmdLine, err := dev.StorageConfig.ToCmdLine()
494
return []string{}, err
496
if len(cmdLine) != 2 {
497
return []string{}, fmt.Errorf("unexpected storage config commandline")
499
if dev.DeviceIdentifier != "" {
500
cmdLine[1] = fmt.Sprintf("%s,deviceId=%s", cmdLine[1], dev.DeviceIdentifier)
505
// VirtioVsockNew creates a new virtio-vsock device for 2-way communication
506
// between the host and the virtual machine. The communication will happen on
507
// vsock port, and on the host it will use the unix socket at socketURL.
508
// When listen is true, the host will be listening for connections over vsock.
509
// When listen is false, the guest will be listening for connections over vsock.
510
func VirtioVsockNew(port uint, socketURL string, listen bool) (VirtioDevice, error) {
513
SocketURL: socketURL,
518
func (dev *VirtioVsock) ToCmdLine() ([]string, error) {
519
if dev.Port == 0 || dev.SocketURL == "" {
520
return nil, fmt.Errorf("virtio-vsock needs both a port and a socket URL")
526
listenStr = "connect"
528
return []string{"--device", fmt.Sprintf("virtio-vsock,port=%d,socketURL=%s,%s", dev.Port, dev.SocketURL, listenStr)}, nil
531
func (dev *VirtioVsock) FromOptions(options []option) error {
532
// default to listen for backwards compatibliity
534
for _, option := range options {
537
dev.SocketURL = option.value
539
port, err := strconv.Atoi(option.value)
543
dev.Port = uint(port)
549
return fmt.Errorf("unknown option for virtio-vsock devices: %s", option.key)
555
// VirtioFsNew creates a new virtio-fs device for file sharing. It will share
556
// the directory at sharedDir with the virtual machine. This directory can be
557
// mounted in the VM using `mount -t virtiofs mountTag /some/dir`
558
func VirtioFsNew(sharedDir string, mountTag string) (VirtioDevice, error) {
560
DirectorySharingConfig: DirectorySharingConfig{
563
SharedDir: sharedDir,
567
func (dev *VirtioFs) ToCmdLine() ([]string, error) {
568
if dev.SharedDir == "" {
569
return nil, fmt.Errorf("virtio-fs needs the path to the directory to share")
571
if dev.MountTag != "" {
572
return []string{"--device", fmt.Sprintf("virtio-fs,sharedDir=%s,mountTag=%s", dev.SharedDir, dev.MountTag)}, nil
575
return []string{"--device", fmt.Sprintf("virtio-fs,sharedDir=%s", dev.SharedDir)}, nil
578
func (dev *VirtioFs) FromOptions(options []option) error {
579
for _, option := range options {
582
dev.SharedDir = option.value
584
dev.MountTag = option.value
586
return fmt.Errorf("unknown option for virtio-fs devices: %s", option.key)
592
// RosettaShare creates a new rosetta share for running x86_64 binaries on M1 machines.
593
// It will share a directory containing the linux rosetta binaries with the
594
// virtual machine. This directory can be mounted in the VM using `mount -t
595
// virtiofs mountTag /some/dir`
596
func RosettaShareNew(mountTag string) (VirtioDevice, error) {
597
return &RosettaShare{
598
DirectorySharingConfig: DirectorySharingConfig{
604
func (dev *RosettaShare) ToCmdLine() ([]string, error) {
605
if dev.MountTag == "" {
606
return nil, fmt.Errorf("rosetta shares require a mount tag to be specified")
608
builder := strings.Builder{}
609
builder.WriteString("rosetta")
610
fmt.Fprintf(&builder, ",mountTag=%s", dev.MountTag)
611
if dev.InstallRosetta {
612
builder.WriteString(",install")
615
return []string{"--device", builder.String()}, nil
618
func (dev *RosettaShare) FromOptions(options []option) error {
619
for _, option := range options {
622
dev.MountTag = option.value
624
dev.InstallRosetta = true
626
return fmt.Errorf("unknown option for rosetta share: %s", option.key)
632
type USBMassStorage struct {
636
func usbMassStorageNewEmpty() *USBMassStorage {
637
return &USBMassStorage{
639
DevName: "usb-mass-storage",
644
// USBMassStorageNew creates a new USB disk to use in the virtual machine. It will use
645
// the file at imagePath as the disk image. This image must be in raw or ISO format.
646
func USBMassStorageNew(imagePath string) (VMComponent, error) {
647
usbMassStorage := usbMassStorageNewEmpty()
648
usbMassStorage.ImagePath = imagePath
650
return usbMassStorage, nil
653
// StorageConfig configures a disk device.
654
type StorageConfig struct {
660
func (config *StorageConfig) ToCmdLine() ([]string, error) {
661
if config.ImagePath == "" {
662
return nil, fmt.Errorf("%s devices need the path to a disk image", config.DevName)
664
return []string{"--device", fmt.Sprintf("%s,path=%s", config.DevName, config.ImagePath)}, nil
667
func (config *StorageConfig) FromOptions(options []option) error {
668
for _, option := range options {
671
config.ImagePath = option.value
673
return fmt.Errorf("unknown option for %s devices: %s", config.DevName, option.key)