1/*
2Copyright (c) 2015-2017 VMware, Inc. All Rights Reserved.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package object
18
19import (
20	"errors"
21	"fmt"
22	"path/filepath"
23	"reflect"
24	"regexp"
25	"sort"
26	"strings"
27
28	"github.com/vmware/govmomi/vim25/types"
29)
30
31// Type values for use in BootOrder
32const (
33	DeviceTypeNone     = "-"
34	DeviceTypeCdrom    = "cdrom"
35	DeviceTypeDisk     = "disk"
36	DeviceTypeEthernet = "ethernet"
37	DeviceTypeFloppy   = "floppy"
38)
39
40// VirtualDeviceList provides helper methods for working with a list of virtual devices.
41type VirtualDeviceList []types.BaseVirtualDevice
42
43// SCSIControllerTypes are used for adding a new SCSI controller to a VM.
44func SCSIControllerTypes() VirtualDeviceList {
45	// Return a mutable list of SCSI controller types, initialized with defaults.
46	return VirtualDeviceList([]types.BaseVirtualDevice{
47		&types.VirtualLsiLogicController{},
48		&types.VirtualBusLogicController{},
49		&types.ParaVirtualSCSIController{},
50		&types.VirtualLsiLogicSASController{},
51	}).Select(func(device types.BaseVirtualDevice) bool {
52		c := device.(types.BaseVirtualSCSIController).GetVirtualSCSIController()
53		c.SharedBus = types.VirtualSCSISharingNoSharing
54		c.BusNumber = -1
55		return true
56	})
57}
58
59// EthernetCardTypes are used for adding a new ethernet card to a VM.
60func EthernetCardTypes() VirtualDeviceList {
61	return VirtualDeviceList([]types.BaseVirtualDevice{
62		&types.VirtualE1000{},
63		&types.VirtualE1000e{},
64		&types.VirtualVmxnet2{},
65		&types.VirtualVmxnet3{},
66		&types.VirtualPCNet32{},
67		&types.VirtualSriovEthernetCard{},
68	}).Select(func(device types.BaseVirtualDevice) bool {
69		c := device.(types.BaseVirtualEthernetCard).GetVirtualEthernetCard()
70		c.GetVirtualDevice().Key = -1
71		return true
72	})
73}
74
75// Select returns a new list containing all elements of the list for which the given func returns true.
76func (l VirtualDeviceList) Select(f func(device types.BaseVirtualDevice) bool) VirtualDeviceList {
77	var found VirtualDeviceList
78
79	for _, device := range l {
80		if f(device) {
81			found = append(found, device)
82		}
83	}
84
85	return found
86}
87
88// SelectByType returns a new list with devices that are equal to or extend the given type.
89func (l VirtualDeviceList) SelectByType(deviceType types.BaseVirtualDevice) VirtualDeviceList {
90	dtype := reflect.TypeOf(deviceType)
91	if dtype == nil {
92		return nil
93	}
94	dname := dtype.Elem().Name()
95
96	return l.Select(func(device types.BaseVirtualDevice) bool {
97		t := reflect.TypeOf(device)
98
99		if t == dtype {
100			return true
101		}
102
103		_, ok := t.Elem().FieldByName(dname)
104
105		return ok
106	})
107}
108
109// SelectByBackingInfo returns a new list with devices matching the given backing info.
110// If the value of backing is nil, any device with a backing of the same type will be returned.
111func (l VirtualDeviceList) SelectByBackingInfo(backing types.BaseVirtualDeviceBackingInfo) VirtualDeviceList {
112	t := reflect.TypeOf(backing)
113
114	return l.Select(func(device types.BaseVirtualDevice) bool {
115		db := device.GetVirtualDevice().Backing
116		if db == nil {
117			return false
118		}
119
120		if reflect.TypeOf(db) != t {
121			return false
122		}
123
124		if reflect.ValueOf(backing).IsNil() {
125			// selecting by backing type
126			return true
127		}
128
129		switch a := db.(type) {
130		case *types.VirtualEthernetCardNetworkBackingInfo:
131			b := backing.(*types.VirtualEthernetCardNetworkBackingInfo)
132			return a.DeviceName == b.DeviceName
133		case *types.VirtualEthernetCardDistributedVirtualPortBackingInfo:
134			b := backing.(*types.VirtualEthernetCardDistributedVirtualPortBackingInfo)
135			return a.Port.SwitchUuid == b.Port.SwitchUuid &&
136				a.Port.PortgroupKey == b.Port.PortgroupKey
137		case *types.VirtualDiskFlatVer2BackingInfo:
138			b := backing.(*types.VirtualDiskFlatVer2BackingInfo)
139			if a.Parent != nil && b.Parent != nil {
140				return a.Parent.FileName == b.Parent.FileName
141			}
142			return a.FileName == b.FileName
143		case *types.VirtualSerialPortURIBackingInfo:
144			b := backing.(*types.VirtualSerialPortURIBackingInfo)
145			return a.ServiceURI == b.ServiceURI
146		case types.BaseVirtualDeviceFileBackingInfo:
147			b := backing.(types.BaseVirtualDeviceFileBackingInfo)
148			return a.GetVirtualDeviceFileBackingInfo().FileName == b.GetVirtualDeviceFileBackingInfo().FileName
149		default:
150			return false
151		}
152	})
153}
154
155// Find returns the device matching the given name.
156func (l VirtualDeviceList) Find(name string) types.BaseVirtualDevice {
157	for _, device := range l {
158		if l.Name(device) == name {
159			return device
160		}
161	}
162	return nil
163}
164
165// FindByKey returns the device matching the given key.
166func (l VirtualDeviceList) FindByKey(key int32) types.BaseVirtualDevice {
167	for _, device := range l {
168		if device.GetVirtualDevice().Key == key {
169			return device
170		}
171	}
172	return nil
173}
174
175// FindIDEController will find the named IDE controller if given, otherwise will pick an available controller.
176// An error is returned if the named controller is not found or not an IDE controller.  Or, if name is not
177// given and no available controller can be found.
178func (l VirtualDeviceList) FindIDEController(name string) (*types.VirtualIDEController, error) {
179	if name != "" {
180		d := l.Find(name)
181		if d == nil {
182			return nil, fmt.Errorf("device '%s' not found", name)
183		}
184		if c, ok := d.(*types.VirtualIDEController); ok {
185			return c, nil
186		}
187		return nil, fmt.Errorf("%s is not an IDE controller", name)
188	}
189
190	c := l.PickController((*types.VirtualIDEController)(nil))
191	if c == nil {
192		return nil, errors.New("no available IDE controller")
193	}
194
195	return c.(*types.VirtualIDEController), nil
196}
197
198// CreateIDEController creates a new IDE controller.
199func (l VirtualDeviceList) CreateIDEController() (types.BaseVirtualDevice, error) {
200	ide := &types.VirtualIDEController{}
201	ide.Key = l.NewKey()
202	return ide, nil
203}
204
205// FindSCSIController will find the named SCSI controller if given, otherwise will pick an available controller.
206// An error is returned if the named controller is not found or not an SCSI controller.  Or, if name is not
207// given and no available controller can be found.
208func (l VirtualDeviceList) FindSCSIController(name string) (*types.VirtualSCSIController, error) {
209	if name != "" {
210		d := l.Find(name)
211		if d == nil {
212			return nil, fmt.Errorf("device '%s' not found", name)
213		}
214		if c, ok := d.(types.BaseVirtualSCSIController); ok {
215			return c.GetVirtualSCSIController(), nil
216		}
217		return nil, fmt.Errorf("%s is not an SCSI controller", name)
218	}
219
220	c := l.PickController((*types.VirtualSCSIController)(nil))
221	if c == nil {
222		return nil, errors.New("no available SCSI controller")
223	}
224
225	return c.(types.BaseVirtualSCSIController).GetVirtualSCSIController(), nil
226}
227
228// CreateSCSIController creates a new SCSI controller of type name if given, otherwise defaults to lsilogic.
229func (l VirtualDeviceList) CreateSCSIController(name string) (types.BaseVirtualDevice, error) {
230	ctypes := SCSIControllerTypes()
231
232	if name == "scsi" || name == "" {
233		name = ctypes.Type(ctypes[0])
234	}
235
236	found := ctypes.Select(func(device types.BaseVirtualDevice) bool {
237		return l.Type(device) == name
238	})
239
240	if len(found) == 0 {
241		return nil, fmt.Errorf("unknown SCSI controller type '%s'", name)
242	}
243
244	c, ok := found[0].(types.BaseVirtualSCSIController)
245	if !ok {
246		return nil, fmt.Errorf("invalid SCSI controller type '%s'", name)
247	}
248
249	scsi := c.GetVirtualSCSIController()
250	scsi.BusNumber = l.newSCSIBusNumber()
251	scsi.Key = l.NewKey()
252	scsi.ScsiCtlrUnitNumber = 7
253	return c.(types.BaseVirtualDevice), nil
254}
255
256var scsiBusNumbers = []int{0, 1, 2, 3}
257
258// newSCSIBusNumber returns the bus number to use for adding a new SCSI bus device.
259// -1 is returned if there are no bus numbers available.
260func (l VirtualDeviceList) newSCSIBusNumber() int32 {
261	var used []int
262
263	for _, d := range l.SelectByType((*types.VirtualSCSIController)(nil)) {
264		num := d.(types.BaseVirtualSCSIController).GetVirtualSCSIController().BusNumber
265		if num >= 0 {
266			used = append(used, int(num))
267		} // else caller is creating a new vm using SCSIControllerTypes
268	}
269
270	sort.Ints(used)
271
272	for i, n := range scsiBusNumbers {
273		if i == len(used) || n != used[i] {
274			return int32(n)
275		}
276	}
277
278	return -1
279}
280
281// FindNVMEController will find the named NVME controller if given, otherwise will pick an available controller.
282// An error is returned if the named controller is not found or not an NVME controller.  Or, if name is not
283// given and no available controller can be found.
284func (l VirtualDeviceList) FindNVMEController(name string) (*types.VirtualNVMEController, error) {
285	if name != "" {
286		d := l.Find(name)
287		if d == nil {
288			return nil, fmt.Errorf("device '%s' not found", name)
289		}
290		if c, ok := d.(*types.VirtualNVMEController); ok {
291			return c, nil
292		}
293		return nil, fmt.Errorf("%s is not an NVME controller", name)
294	}
295
296	c := l.PickController((*types.VirtualNVMEController)(nil))
297	if c == nil {
298		return nil, errors.New("no available NVME controller")
299	}
300
301	return c.(*types.VirtualNVMEController), nil
302}
303
304// CreateNVMEController creates a new NVMWE controller.
305func (l VirtualDeviceList) CreateNVMEController() (types.BaseVirtualDevice, error) {
306	nvme := &types.VirtualNVMEController{}
307	nvme.BusNumber = l.newNVMEBusNumber()
308	nvme.Key = l.NewKey()
309
310	return nvme, nil
311}
312
313var nvmeBusNumbers = []int{0, 1, 2, 3}
314
315// newNVMEBusNumber returns the bus number to use for adding a new NVME bus device.
316// -1 is returned if there are no bus numbers available.
317func (l VirtualDeviceList) newNVMEBusNumber() int32 {
318	var used []int
319
320	for _, d := range l.SelectByType((*types.VirtualNVMEController)(nil)) {
321		num := d.(types.BaseVirtualController).GetVirtualController().BusNumber
322		if num >= 0 {
323			used = append(used, int(num))
324		} // else caller is creating a new vm using NVMEControllerTypes
325	}
326
327	sort.Ints(used)
328
329	for i, n := range nvmeBusNumbers {
330		if i == len(used) || n != used[i] {
331			return int32(n)
332		}
333	}
334
335	return -1
336}
337
338// FindDiskController will find an existing ide or scsi disk controller.
339func (l VirtualDeviceList) FindDiskController(name string) (types.BaseVirtualController, error) {
340	switch {
341	case name == "ide":
342		return l.FindIDEController("")
343	case name == "scsi" || name == "":
344		return l.FindSCSIController("")
345	case name == "nvme":
346		return l.FindNVMEController("")
347	default:
348		if c, ok := l.Find(name).(types.BaseVirtualController); ok {
349			return c, nil
350		}
351		return nil, fmt.Errorf("%s is not a valid controller", name)
352	}
353}
354
355// PickController returns a controller of the given type(s).
356// If no controllers are found or have no available slots, then nil is returned.
357func (l VirtualDeviceList) PickController(kind types.BaseVirtualController) types.BaseVirtualController {
358	l = l.SelectByType(kind.(types.BaseVirtualDevice)).Select(func(device types.BaseVirtualDevice) bool {
359		num := len(device.(types.BaseVirtualController).GetVirtualController().Device)
360
361		switch device.(type) {
362		case types.BaseVirtualSCSIController:
363			return num < 15
364		case *types.VirtualIDEController:
365			return num < 2
366		case *types.VirtualNVMEController:
367			return num < 8
368		default:
369			return true
370		}
371	})
372
373	if len(l) == 0 {
374		return nil
375	}
376
377	return l[0].(types.BaseVirtualController)
378}
379
380// newUnitNumber returns the unit number to use for attaching a new device to the given controller.
381func (l VirtualDeviceList) newUnitNumber(c types.BaseVirtualController) int32 {
382	units := make([]bool, 30)
383
384	switch sc := c.(type) {
385	case types.BaseVirtualSCSIController:
386		//  The SCSI controller sits on its own bus
387		units[sc.GetVirtualSCSIController().ScsiCtlrUnitNumber] = true
388	}
389
390	key := c.GetVirtualController().Key
391
392	for _, device := range l {
393		d := device.GetVirtualDevice()
394
395		if d.ControllerKey == key && d.UnitNumber != nil {
396			units[int(*d.UnitNumber)] = true
397		}
398	}
399
400	for unit, used := range units {
401		if !used {
402			return int32(unit)
403		}
404	}
405
406	return -1
407}
408
409// NewKey returns the key to use for adding a new device to the device list.
410// The device list we're working with here may not be complete (e.g. when
411// we're only adding new devices), so any positive keys could conflict with device keys
412// that are already in use. To avoid this type of conflict, we can use negative keys
413// here, which will be resolved to positive keys by vSphere as the reconfiguration is done.
414func (l VirtualDeviceList) NewKey() int32 {
415	var key int32 = -200
416
417	for _, device := range l {
418		d := device.GetVirtualDevice()
419		if d.Key < key {
420			key = d.Key
421		}
422	}
423
424	return key - 1
425}
426
427// AssignController assigns a device to a controller.
428func (l VirtualDeviceList) AssignController(device types.BaseVirtualDevice, c types.BaseVirtualController) {
429	d := device.GetVirtualDevice()
430	d.ControllerKey = c.GetVirtualController().Key
431	d.UnitNumber = new(int32)
432	*d.UnitNumber = l.newUnitNumber(c)
433	if d.Key == 0 {
434		d.Key = -1
435	}
436}
437
438// CreateDisk creates a new VirtualDisk device which can be added to a VM.
439func (l VirtualDeviceList) CreateDisk(c types.BaseVirtualController, ds types.ManagedObjectReference, name string) *types.VirtualDisk {
440	// If name is not specified, one will be chosen for you.
441	// But if when given, make sure it ends in .vmdk, otherwise it will be treated as a directory.
442	if len(name) > 0 && filepath.Ext(name) != ".vmdk" {
443		name += ".vmdk"
444	}
445
446	device := &types.VirtualDisk{
447		VirtualDevice: types.VirtualDevice{
448			Backing: &types.VirtualDiskFlatVer2BackingInfo{
449				DiskMode:        string(types.VirtualDiskModePersistent),
450				ThinProvisioned: types.NewBool(true),
451				VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
452					FileName:  name,
453					Datastore: &ds,
454				},
455			},
456		},
457	}
458
459	l.AssignController(device, c)
460	return device
461}
462
463// ChildDisk creates a new VirtualDisk device, linked to the given parent disk, which can be added to a VM.
464func (l VirtualDeviceList) ChildDisk(parent *types.VirtualDisk) *types.VirtualDisk {
465	disk := *parent
466	backing := disk.Backing.(*types.VirtualDiskFlatVer2BackingInfo)
467	p := new(DatastorePath)
468	p.FromString(backing.FileName)
469	p.Path = ""
470
471	// Use specified disk as parent backing to a new disk.
472	disk.Backing = &types.VirtualDiskFlatVer2BackingInfo{
473		VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
474			FileName:  p.String(),
475			Datastore: backing.Datastore,
476		},
477		Parent:          backing,
478		DiskMode:        backing.DiskMode,
479		ThinProvisioned: backing.ThinProvisioned,
480	}
481
482	return &disk
483}
484
485func (l VirtualDeviceList) connectivity(device types.BaseVirtualDevice, v bool) error {
486	c := device.GetVirtualDevice().Connectable
487	if c == nil {
488		return fmt.Errorf("%s is not connectable", l.Name(device))
489	}
490
491	c.Connected = v
492	c.StartConnected = v
493
494	return nil
495}
496
497// Connect changes the device to connected, returns an error if the device is not connectable.
498func (l VirtualDeviceList) Connect(device types.BaseVirtualDevice) error {
499	return l.connectivity(device, true)
500}
501
502// Disconnect changes the device to disconnected, returns an error if the device is not connectable.
503func (l VirtualDeviceList) Disconnect(device types.BaseVirtualDevice) error {
504	return l.connectivity(device, false)
505}
506
507// FindCdrom finds a cdrom device with the given name, defaulting to the first cdrom device if any.
508func (l VirtualDeviceList) FindCdrom(name string) (*types.VirtualCdrom, error) {
509	if name != "" {
510		d := l.Find(name)
511		if d == nil {
512			return nil, fmt.Errorf("device '%s' not found", name)
513		}
514		if c, ok := d.(*types.VirtualCdrom); ok {
515			return c, nil
516		}
517		return nil, fmt.Errorf("%s is not a cdrom device", name)
518	}
519
520	c := l.SelectByType((*types.VirtualCdrom)(nil))
521	if len(c) == 0 {
522		return nil, errors.New("no cdrom device found")
523	}
524
525	return c[0].(*types.VirtualCdrom), nil
526}
527
528// CreateCdrom creates a new VirtualCdrom device which can be added to a VM.
529func (l VirtualDeviceList) CreateCdrom(c *types.VirtualIDEController) (*types.VirtualCdrom, error) {
530	device := &types.VirtualCdrom{}
531
532	l.AssignController(device, c)
533
534	l.setDefaultCdromBacking(device)
535
536	device.Connectable = &types.VirtualDeviceConnectInfo{
537		AllowGuestControl: true,
538		Connected:         true,
539		StartConnected:    true,
540	}
541
542	return device, nil
543}
544
545// InsertIso changes the cdrom device backing to use the given iso file.
546func (l VirtualDeviceList) InsertIso(device *types.VirtualCdrom, iso string) *types.VirtualCdrom {
547	device.Backing = &types.VirtualCdromIsoBackingInfo{
548		VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
549			FileName: iso,
550		},
551	}
552
553	return device
554}
555
556// EjectIso removes the iso file based backing and replaces with the default cdrom backing.
557func (l VirtualDeviceList) EjectIso(device *types.VirtualCdrom) *types.VirtualCdrom {
558	l.setDefaultCdromBacking(device)
559	return device
560}
561
562func (l VirtualDeviceList) setDefaultCdromBacking(device *types.VirtualCdrom) {
563	device.Backing = &types.VirtualCdromAtapiBackingInfo{
564		VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{
565			DeviceName:    fmt.Sprintf("%s-%d-%d", DeviceTypeCdrom, device.ControllerKey, device.UnitNumber),
566			UseAutoDetect: types.NewBool(false),
567		},
568	}
569}
570
571// FindFloppy finds a floppy device with the given name, defaulting to the first floppy device if any.
572func (l VirtualDeviceList) FindFloppy(name string) (*types.VirtualFloppy, error) {
573	if name != "" {
574		d := l.Find(name)
575		if d == nil {
576			return nil, fmt.Errorf("device '%s' not found", name)
577		}
578		if c, ok := d.(*types.VirtualFloppy); ok {
579			return c, nil
580		}
581		return nil, fmt.Errorf("%s is not a floppy device", name)
582	}
583
584	c := l.SelectByType((*types.VirtualFloppy)(nil))
585	if len(c) == 0 {
586		return nil, errors.New("no floppy device found")
587	}
588
589	return c[0].(*types.VirtualFloppy), nil
590}
591
592// CreateFloppy creates a new VirtualFloppy device which can be added to a VM.
593func (l VirtualDeviceList) CreateFloppy() (*types.VirtualFloppy, error) {
594	device := &types.VirtualFloppy{}
595
596	c := l.PickController((*types.VirtualSIOController)(nil))
597	if c == nil {
598		return nil, errors.New("no available SIO controller")
599	}
600
601	l.AssignController(device, c)
602
603	l.setDefaultFloppyBacking(device)
604
605	device.Connectable = &types.VirtualDeviceConnectInfo{
606		AllowGuestControl: true,
607		Connected:         true,
608		StartConnected:    true,
609	}
610
611	return device, nil
612}
613
614// InsertImg changes the floppy device backing to use the given img file.
615func (l VirtualDeviceList) InsertImg(device *types.VirtualFloppy, img string) *types.VirtualFloppy {
616	device.Backing = &types.VirtualFloppyImageBackingInfo{
617		VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
618			FileName: img,
619		},
620	}
621
622	return device
623}
624
625// EjectImg removes the img file based backing and replaces with the default floppy backing.
626func (l VirtualDeviceList) EjectImg(device *types.VirtualFloppy) *types.VirtualFloppy {
627	l.setDefaultFloppyBacking(device)
628	return device
629}
630
631func (l VirtualDeviceList) setDefaultFloppyBacking(device *types.VirtualFloppy) {
632	device.Backing = &types.VirtualFloppyDeviceBackingInfo{
633		VirtualDeviceDeviceBackingInfo: types.VirtualDeviceDeviceBackingInfo{
634			DeviceName:    fmt.Sprintf("%s-%d", DeviceTypeFloppy, device.UnitNumber),
635			UseAutoDetect: types.NewBool(false),
636		},
637	}
638}
639
640// FindSerialPort finds a serial port device with the given name, defaulting to the first serial port device if any.
641func (l VirtualDeviceList) FindSerialPort(name string) (*types.VirtualSerialPort, error) {
642	if name != "" {
643		d := l.Find(name)
644		if d == nil {
645			return nil, fmt.Errorf("device '%s' not found", name)
646		}
647		if c, ok := d.(*types.VirtualSerialPort); ok {
648			return c, nil
649		}
650		return nil, fmt.Errorf("%s is not a serial port device", name)
651	}
652
653	c := l.SelectByType((*types.VirtualSerialPort)(nil))
654	if len(c) == 0 {
655		return nil, errors.New("no serial port device found")
656	}
657
658	return c[0].(*types.VirtualSerialPort), nil
659}
660
661// CreateSerialPort creates a new VirtualSerialPort device which can be added to a VM.
662func (l VirtualDeviceList) CreateSerialPort() (*types.VirtualSerialPort, error) {
663	device := &types.VirtualSerialPort{
664		YieldOnPoll: true,
665	}
666
667	c := l.PickController((*types.VirtualSIOController)(nil))
668	if c == nil {
669		return nil, errors.New("no available SIO controller")
670	}
671
672	l.AssignController(device, c)
673
674	l.setDefaultSerialPortBacking(device)
675
676	return device, nil
677}
678
679// ConnectSerialPort connects a serial port to a server or client uri.
680func (l VirtualDeviceList) ConnectSerialPort(device *types.VirtualSerialPort, uri string, client bool, proxyuri string) *types.VirtualSerialPort {
681	if strings.HasPrefix(uri, "[") {
682		device.Backing = &types.VirtualSerialPortFileBackingInfo{
683			VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
684				FileName: uri,
685			},
686		}
687
688		return device
689	}
690
691	direction := types.VirtualDeviceURIBackingOptionDirectionServer
692	if client {
693		direction = types.VirtualDeviceURIBackingOptionDirectionClient
694	}
695
696	device.Backing = &types.VirtualSerialPortURIBackingInfo{
697		VirtualDeviceURIBackingInfo: types.VirtualDeviceURIBackingInfo{
698			Direction:  string(direction),
699			ServiceURI: uri,
700			ProxyURI:   proxyuri,
701		},
702	}
703
704	return device
705}
706
707// DisconnectSerialPort disconnects the serial port backing.
708func (l VirtualDeviceList) DisconnectSerialPort(device *types.VirtualSerialPort) *types.VirtualSerialPort {
709	l.setDefaultSerialPortBacking(device)
710	return device
711}
712
713func (l VirtualDeviceList) setDefaultSerialPortBacking(device *types.VirtualSerialPort) {
714	device.Backing = &types.VirtualSerialPortURIBackingInfo{
715		VirtualDeviceURIBackingInfo: types.VirtualDeviceURIBackingInfo{
716			Direction:  "client",
717			ServiceURI: "localhost:0",
718		},
719	}
720}
721
722// CreateEthernetCard creates a new VirtualEthernetCard of the given name name and initialized with the given backing.
723func (l VirtualDeviceList) CreateEthernetCard(name string, backing types.BaseVirtualDeviceBackingInfo) (types.BaseVirtualDevice, error) {
724	ctypes := EthernetCardTypes()
725
726	if name == "" {
727		name = ctypes.deviceName(ctypes[0])
728	}
729
730	found := ctypes.Select(func(device types.BaseVirtualDevice) bool {
731		return l.deviceName(device) == name
732	})
733
734	if len(found) == 0 {
735		return nil, fmt.Errorf("unknown ethernet card type '%s'", name)
736	}
737
738	c, ok := found[0].(types.BaseVirtualEthernetCard)
739	if !ok {
740		return nil, fmt.Errorf("invalid ethernet card type '%s'", name)
741	}
742
743	c.GetVirtualEthernetCard().Backing = backing
744
745	return c.(types.BaseVirtualDevice), nil
746}
747
748// PrimaryMacAddress returns the MacAddress field of the primary VirtualEthernetCard
749func (l VirtualDeviceList) PrimaryMacAddress() string {
750	eth0 := l.Find("ethernet-0")
751
752	if eth0 == nil {
753		return ""
754	}
755
756	return eth0.(types.BaseVirtualEthernetCard).GetVirtualEthernetCard().MacAddress
757}
758
759// convert a BaseVirtualDevice to a BaseVirtualMachineBootOptionsBootableDevice
760var bootableDevices = map[string]func(device types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice{
761	DeviceTypeNone: func(types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
762		return &types.VirtualMachineBootOptionsBootableDevice{}
763	},
764	DeviceTypeCdrom: func(types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
765		return &types.VirtualMachineBootOptionsBootableCdromDevice{}
766	},
767	DeviceTypeDisk: func(d types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
768		return &types.VirtualMachineBootOptionsBootableDiskDevice{
769			DeviceKey: d.GetVirtualDevice().Key,
770		}
771	},
772	DeviceTypeEthernet: func(d types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
773		return &types.VirtualMachineBootOptionsBootableEthernetDevice{
774			DeviceKey: d.GetVirtualDevice().Key,
775		}
776	},
777	DeviceTypeFloppy: func(types.BaseVirtualDevice) types.BaseVirtualMachineBootOptionsBootableDevice {
778		return &types.VirtualMachineBootOptionsBootableFloppyDevice{}
779	},
780}
781
782// BootOrder returns a list of devices which can be used to set boot order via VirtualMachine.SetBootOptions.
783// The order can be any of "ethernet", "cdrom", "floppy" or "disk" or by specific device name.
784// A value of "-" will clear the existing boot order on the VC/ESX side.
785func (l VirtualDeviceList) BootOrder(order []string) []types.BaseVirtualMachineBootOptionsBootableDevice {
786	var devices []types.BaseVirtualMachineBootOptionsBootableDevice
787
788	for _, name := range order {
789		if kind, ok := bootableDevices[name]; ok {
790			if name == DeviceTypeNone {
791				// Not covered in the API docs, nor obvious, but this clears the boot order on the VC/ESX side.
792				devices = append(devices, new(types.VirtualMachineBootOptionsBootableDevice))
793				continue
794			}
795
796			for _, device := range l {
797				if l.Type(device) == name {
798					devices = append(devices, kind(device))
799				}
800			}
801			continue
802		}
803
804		if d := l.Find(name); d != nil {
805			if kind, ok := bootableDevices[l.Type(d)]; ok {
806				devices = append(devices, kind(d))
807			}
808		}
809	}
810
811	return devices
812}
813
814// SelectBootOrder returns an ordered list of devices matching the given bootable device order
815func (l VirtualDeviceList) SelectBootOrder(order []types.BaseVirtualMachineBootOptionsBootableDevice) VirtualDeviceList {
816	var devices VirtualDeviceList
817
818	for _, bd := range order {
819		for _, device := range l {
820			if kind, ok := bootableDevices[l.Type(device)]; ok {
821				if reflect.DeepEqual(kind(device), bd) {
822					devices = append(devices, device)
823				}
824			}
825		}
826	}
827
828	return devices
829}
830
831// TypeName returns the vmodl type name of the device
832func (l VirtualDeviceList) TypeName(device types.BaseVirtualDevice) string {
833	dtype := reflect.TypeOf(device)
834	if dtype == nil {
835		return ""
836	}
837	return dtype.Elem().Name()
838}
839
840var deviceNameRegexp = regexp.MustCompile(`(?:Virtual)?(?:Machine)?(\w+?)(?:Card|EthernetCard|Device|Controller)?$`)
841
842func (l VirtualDeviceList) deviceName(device types.BaseVirtualDevice) string {
843	name := "device"
844	typeName := l.TypeName(device)
845
846	m := deviceNameRegexp.FindStringSubmatch(typeName)
847	if len(m) == 2 {
848		name = strings.ToLower(m[1])
849	}
850
851	return name
852}
853
854// Type returns a human-readable name for the given device
855func (l VirtualDeviceList) Type(device types.BaseVirtualDevice) string {
856	switch device.(type) {
857	case types.BaseVirtualEthernetCard:
858		return DeviceTypeEthernet
859	case *types.ParaVirtualSCSIController:
860		return "pvscsi"
861	case *types.VirtualLsiLogicSASController:
862		return "lsilogic-sas"
863	case *types.VirtualNVMEController:
864		return "nvme"
865	default:
866		return l.deviceName(device)
867	}
868}
869
870// Name returns a stable, human-readable name for the given device
871func (l VirtualDeviceList) Name(device types.BaseVirtualDevice) string {
872	var key string
873	var UnitNumber int32
874	d := device.GetVirtualDevice()
875	if d.UnitNumber != nil {
876		UnitNumber = *d.UnitNumber
877	}
878
879	dtype := l.Type(device)
880	switch dtype {
881	case DeviceTypeEthernet:
882		key = fmt.Sprintf("%d", UnitNumber-7)
883	case DeviceTypeDisk:
884		key = fmt.Sprintf("%d-%d", d.ControllerKey, UnitNumber)
885	default:
886		key = fmt.Sprintf("%d", d.Key)
887	}
888
889	return fmt.Sprintf("%s-%s", dtype, key)
890}
891
892// ConfigSpec creates a virtual machine configuration spec for
893// the specified operation, for the list of devices in the device list.
894func (l VirtualDeviceList) ConfigSpec(op types.VirtualDeviceConfigSpecOperation) ([]types.BaseVirtualDeviceConfigSpec, error) {
895	var fop types.VirtualDeviceConfigSpecFileOperation
896	switch op {
897	case types.VirtualDeviceConfigSpecOperationAdd:
898		fop = types.VirtualDeviceConfigSpecFileOperationCreate
899	case types.VirtualDeviceConfigSpecOperationEdit:
900		fop = types.VirtualDeviceConfigSpecFileOperationReplace
901	case types.VirtualDeviceConfigSpecOperationRemove:
902		fop = types.VirtualDeviceConfigSpecFileOperationDestroy
903	default:
904		panic("unknown op")
905	}
906
907	var res []types.BaseVirtualDeviceConfigSpec
908	for _, device := range l {
909		config := &types.VirtualDeviceConfigSpec{
910			Device:    device,
911			Operation: op,
912		}
913
914		if disk, ok := device.(*types.VirtualDisk); ok {
915			config.FileOperation = fop
916
917			// Special case to attach an existing disk
918			if op == types.VirtualDeviceConfigSpecOperationAdd && disk.CapacityInKB == 0 {
919				childDisk := false
920				if b, ok := disk.Backing.(*types.VirtualDiskFlatVer2BackingInfo); ok {
921					childDisk = b.Parent != nil
922				}
923
924				if !childDisk {
925					// Existing disk, clear file operation
926					config.FileOperation = ""
927				}
928			}
929		}
930
931		res = append(res, config)
932	}
933
934	return res, nil
935}
936