forked from vmware/go-vcloud-director
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvm.go
1855 lines (1536 loc) · 64.7 KB
/
vm.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License.
*/
package govcd
import (
"fmt"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/vmware/go-vcloud-director/v2/types/v56"
"github.com/vmware/go-vcloud-director/v2/util"
)
type VM struct {
VM *types.Vm
client *Client
}
type VMRecord struct {
VM *types.QueryResultVMRecordType
client *Client
}
func NewVM(cli *Client) *VM {
return &VM{
VM: new(types.Vm),
client: cli,
}
}
// NewVMRecord creates an instance with reference to types.QueryResultVMRecordType
func NewVMRecord(cli *Client) *VMRecord {
return &VMRecord{
VM: new(types.QueryResultVMRecordType),
client: cli,
}
}
func (vm *VM) GetStatus() (string, error) {
err := vm.Refresh()
if err != nil {
return "", fmt.Errorf("error refreshing VM: %s", err)
}
return types.VAppStatuses[vm.VM.Status], nil
}
// IsDeployed checks if the VM is deployed or not
func (vm *VM) IsDeployed() (bool, error) {
err := vm.Refresh()
if err != nil {
return false, fmt.Errorf("error refreshing VM: %s", err)
}
return vm.VM.Deployed, nil
}
func (vm *VM) Refresh() error {
if vm.VM.HREF == "" {
return fmt.Errorf("cannot refresh VM, Object is empty")
}
refreshUrl := vm.VM.HREF
// Empty struct before a new unmarshal, otherwise we end up with duplicate
// elements in slices.
vm.VM = &types.Vm{}
_, err := vm.client.ExecuteRequest(refreshUrl, http.MethodGet, "", "error refreshing VM: %s", nil, vm.VM)
// The request was successful
return err
}
// GetVirtualHardwareSection returns the virtual hardware items attached to a VM
func (vm *VM) GetVirtualHardwareSection() (*types.VirtualHardwareSection, error) {
virtualHardwareSection := &types.VirtualHardwareSection{}
if vm.VM.HREF == "" {
return nil, fmt.Errorf("cannot refresh, invalid reference url")
}
_, err := vm.client.ExecuteRequest(vm.VM.HREF+"/virtualHardwareSection/", http.MethodGet,
types.MimeVirtualHardwareSection, "error retrieving virtual hardware: %s", nil, virtualHardwareSection)
// The request was successful
return virtualHardwareSection, err
}
// GetNetworkConnectionSection returns current networks attached to VM
//
// The slice of NICs is not necessarily ordered by NIC index
func (vm *VM) GetNetworkConnectionSection() (*types.NetworkConnectionSection, error) {
networkConnectionSection := &types.NetworkConnectionSection{}
if vm.VM.HREF == "" {
return networkConnectionSection, fmt.Errorf("cannot retrieve network when VM HREF is unset")
}
_, err := vm.client.ExecuteRequest(vm.VM.HREF+"/networkConnectionSection/", http.MethodGet,
types.MimeNetworkConnectionSection, "error retrieving network connection: %s", nil, networkConnectionSection)
// The request was successful
return networkConnectionSection, err
}
// UpdateNetworkConnectionSection applies network configuration of types.NetworkConnectionSection for the VM
// Runs synchronously, VM is ready for another operation after this function returns.
func (vm *VM) UpdateNetworkConnectionSection(networks *types.NetworkConnectionSection) error {
if vm.VM.HREF == "" {
return fmt.Errorf("cannot update network connection when VM HREF is unset")
}
// Retrieve current network configuration so that we are not altering any other internal fields
updateNetwork, err := vm.GetNetworkConnectionSection()
if err != nil {
return fmt.Errorf("cannot read network section for update: %s", err)
}
updateNetwork.PrimaryNetworkConnectionIndex = networks.PrimaryNetworkConnectionIndex
updateNetwork.NetworkConnection = networks.NetworkConnection
updateNetwork.Ovf = types.XMLNamespaceOVF
task, err := vm.client.ExecuteTaskRequest(vm.VM.HREF+"/networkConnectionSection/", http.MethodPut,
types.MimeNetworkConnectionSection, "error updating network connection: %s", updateNetwork)
if err != nil {
return err
}
err = task.WaitTaskCompletion()
if err != nil {
return fmt.Errorf("error waiting for task completion after network update for vm %s: %s", vm.VM.Name, err)
}
return nil
}
// Deprecated: use client.GetVMByHref instead
func (client *Client) FindVMByHREF(vmHREF string) (VM, error) {
newVm := NewVM(client)
_, err := client.ExecuteRequest(vmHREF, http.MethodGet,
"", "error retrieving VM: %s", nil, newVm.VM)
return *newVm, err
}
func (vm *VM) PowerOn() (Task, error) {
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/power/action/powerOn"
// Return the task
return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost,
"", "error powering on VM: %s", nil)
}
// PowerOnAndForceCustomization is a synchronous function which is equivalent to the functionality
// one has in UI. It triggers customization which may be useful in some cases (like altering NICs)
//
// The VM _must_ be un-deployed for this action to actually work.
func (vm *VM) PowerOnAndForceCustomization() error {
// PowerOnAndForceCustomization only works if the VM was previously un-deployed
vmIsDeployed, err := vm.IsDeployed()
if err != nil {
return fmt.Errorf("unable to check if VM %s is un-deployed forcing customization: %s",
vm.VM.Name, err)
}
if vmIsDeployed {
return fmt.Errorf("VM %s must be undeployed before forcing customization", vm.VM.Name)
}
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/action/deploy"
powerOnAndCustomize := &types.DeployVAppParams{
Xmlns: types.XMLNamespaceVCloud,
PowerOn: true,
ForceCustomization: true,
}
task, err := vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost,
"", "error powering on VM with customization: %s", powerOnAndCustomize)
if err != nil {
return err
}
err = task.WaitTaskCompletion()
if err != nil {
return fmt.Errorf("error waiting for task completion after power on with customization %s: %s", vm.VM.Name, err)
}
return nil
}
func (vm *VM) PowerOff() (Task, error) {
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/power/action/powerOff"
// Return the task
return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost,
"", "error powering off VM: %s", nil)
}
// ChangeCPUCount sets number of available virtual logical processors
// (i.e. CPUs x cores per socket)
// Cpu cores count is inherited from template.
// https://communities.vmware.com/thread/576209
// Deprecated: use vm.ChangeCPU instead
func (vm *VM) ChangeCPUCount(virtualCpuCount int) (Task, error) {
return vm.ChangeCPUCountWithCore(virtualCpuCount, nil)
}
// ChangeCPUCountWithCore sets number of available virtual logical processors
// (i.e. CPUs x cores per socket) and cores per socket.
// Socket count is a result of: virtual logical processors/cores per socket
// https://communities.vmware.com/thread/576209
// Deprecated: use vm.ChangeCPU instead
func (vm *VM) ChangeCPUCountWithCore(virtualCpuCount int, coresPerSocket *int) (Task, error) {
err := vm.Refresh()
if err != nil {
return Task{}, fmt.Errorf("error refreshing VM before running customization: %s", err)
}
newCpu := &types.OVFItem{
XmlnsRasd: types.XMLNamespaceRASD,
XmlnsVCloud: types.XMLNamespaceVCloud,
XmlnsXsi: types.XMLNamespaceXSI,
XmlnsVmw: types.XMLNamespaceVMW,
VCloudHREF: vm.VM.HREF + "/virtualHardwareSection/cpu",
VCloudType: types.MimeRasdItem,
AllocationUnits: "hertz * 10^6",
Description: "Number of Virtual CPUs",
ElementName: strconv.Itoa(virtualCpuCount) + " virtual CPU(s)",
InstanceID: 4,
Reservation: 0,
ResourceType: types.ResourceTypeProcessor,
VirtualQuantity: int64(virtualCpuCount),
CoresPerSocket: coresPerSocket,
Link: &types.Link{
HREF: vm.VM.HREF + "/virtualHardwareSection/cpu",
Rel: "edit",
Type: types.MimeRasdItem,
},
}
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/virtualHardwareSection/cpu"
// Return the task
return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPut,
types.MimeRasdItem, "error changing CPU count: %s", newCpu)
}
func (vm *VM) updateNicParameters(networks []map[string]interface{}, networkSection *types.NetworkConnectionSection) error {
for tfNicSlot, network := range networks {
for loopIndex := range networkSection.NetworkConnection {
// Change network config only if we have the same virtual slot number as in .tf config
if tfNicSlot == networkSection.NetworkConnection[loopIndex].NetworkConnectionIndex {
// Determine what type of address is requested for the vApp
var ipAllocationMode string
ipAddress := "Any"
var ipFieldString string
ipField, ipIsSet := network["ip"]
if ipIsSet {
ipFieldString = ipField.(string)
}
switch {
// TODO v3.0 remove from here when deprecated `ip` and `network_name` attributes are removed
case ipIsSet && ipFieldString == "dhcp": // Deprecated ip="dhcp" mode
ipAllocationMode = types.IPAllocationModeDHCP
case ipIsSet && ipFieldString == "allocated": // Deprecated ip="allocated" mode
ipAllocationMode = types.IPAllocationModePool
case ipIsSet && ipFieldString == "none": // Deprecated ip="none" mode
ipAllocationMode = types.IPAllocationModeNone
// Deprecated ip="valid_ip" mode (currently it is hit by ip_allocation_mode=MANUAL as well)
case ipIsSet && net.ParseIP(ipFieldString) != nil:
ipAllocationMode = types.IPAllocationModeManual
ipAddress = ipFieldString
case ipIsSet && ipFieldString != "": // Deprecated ip="something_invalid" we default to DHCP. This is odd but backwards compatible.
ipAllocationMode = types.IPAllocationModeDHCP
// TODO v3.0 remove until here when deprecated `ip` and `network_name` attributes are removed
// Removed for Coverity warning: dead code - We can reinstate after removing above code
//case ipIsSet && net.ParseIP(ipFieldString) != nil && (network["ip_allocation_mode"].(string) == types.IPAllocationModeManual):
// ipAllocationMode = types.IPAllocationModeManual
// ipAddress = ipFieldString
default: // New networks functionality. IP was not set and we're defaulting to provided ip_allocation_mode (only manual requires the IP)
ipAllocationMode = network["ip_allocation_mode"].(string)
}
networkSection.NetworkConnection[loopIndex].NeedsCustomization = true
networkSection.NetworkConnection[loopIndex].IsConnected = true
networkSection.NetworkConnection[loopIndex].IPAddress = ipAddress
networkSection.NetworkConnection[loopIndex].IPAddressAllocationMode = ipAllocationMode
// for IPAllocationModeNone we hardcode special network name used by vcd 'none'
if ipAllocationMode == types.IPAllocationModeNone {
networkSection.NetworkConnection[loopIndex].Network = types.NoneNetwork
} else {
if _, ok := network["network_name"]; !ok {
return fmt.Errorf("could not identify network name")
}
networkSection.NetworkConnection[loopIndex].Network = network["network_name"].(string)
}
// If we have one NIC only then it is primary by default, otherwise we check for "is_primary" key
if (len(networks) == 1) || (network["is_primary"] != nil && network["is_primary"].(bool)) {
networkSection.PrimaryNetworkConnectionIndex = tfNicSlot
}
}
}
}
return nil
}
// ChangeNetworkConfig allows to update existing VM NIC configuration.f
func (vm *VM) ChangeNetworkConfig(networks []map[string]interface{}) (Task, error) {
err := vm.Refresh()
if err != nil {
return Task{}, fmt.Errorf("error refreshing VM before running customization: %s", err)
}
networkSection, err := vm.GetNetworkConnectionSection()
if err != nil {
return Task{}, fmt.Errorf("could not retrieve network connection for VM: %s", err)
}
err = vm.updateNicParameters(networks, networkSection)
if err != nil {
return Task{}, fmt.Errorf("failed processing NIC parameters: %s", err)
}
networkSection.Xmlns = types.XMLNamespaceVCloud
networkSection.Ovf = types.XMLNamespaceOVF
networkSection.Info = "Specifies the available VM network connections"
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/networkConnectionSection/"
// Return the task
return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPut,
types.MimeNetworkConnectionSection, "error changing network config: %s", networkSection)
}
// Deprecated: use vm.ChangeMemory instead
func (vm *VM) ChangeMemorySize(size int) (Task, error) {
err := vm.Refresh()
if err != nil {
return Task{}, fmt.Errorf("error refreshing VM before running customization: %s", err)
}
newMem := &types.OVFItem{
XmlnsRasd: types.XMLNamespaceRASD,
XmlnsVCloud: types.XMLNamespaceVCloud,
XmlnsXsi: types.XMLNamespaceXSI,
VCloudHREF: vm.VM.HREF + "/virtualHardwareSection/memory",
VCloudType: types.MimeRasdItem,
AllocationUnits: "byte * 2^20",
Description: "Memory SizeMb",
ElementName: strconv.Itoa(size) + " MB of memory",
InstanceID: 5,
Reservation: 0,
ResourceType: types.ResourceTypeMemory,
VirtualQuantity: int64(size),
Weight: 0,
Link: &types.Link{
HREF: vm.VM.HREF + "/virtualHardwareSection/memory",
Rel: "edit",
Type: types.MimeRasdItem,
},
}
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/virtualHardwareSection/memory"
// Return the task
return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPut,
types.MimeRasdItem, "error changing memory size: %s", newMem)
}
func (vm *VM) RunCustomizationScript(computerName, script string) (Task, error) {
return vm.Customize(computerName, script, false)
}
// GetGuestCustomizationStatus retrieves guest customization status.
// It can be one of "GC_PENDING", "REBOOT_PENDING", "GC_FAILED", "POST_GC_PENDING", "GC_COMPLETE"
func (vm *VM) GetGuestCustomizationStatus() (string, error) {
guestCustomizationStatus := &types.GuestCustomizationStatusSection{}
if vm.VM.HREF == "" {
return "", fmt.Errorf("cannot retrieve guest customization, VM HREF is empty")
}
_, err := vm.client.ExecuteRequest(vm.VM.HREF+"/guestcustomizationstatus", http.MethodGet,
types.MimeGuestCustomizationStatus, "error retrieving guest customization status: %s", nil, guestCustomizationStatus)
// The request was successful
return guestCustomizationStatus.GuestCustStatus, err
}
// BlockWhileGuestCustomizationStatus blocks until the customization status of VM exits unwantedStatus.
// It sleeps 3 seconds between iterations and times out after timeOutAfterSeconds of seconds.
//
// timeOutAfterSeconds must be more than 4 and less than 2 hours (60s*120)
func (vm *VM) BlockWhileGuestCustomizationStatus(unwantedStatus string, timeOutAfterSeconds int) error {
if timeOutAfterSeconds < 5 || timeOutAfterSeconds > 60*120 {
return fmt.Errorf("timeOutAfterSeconds must be in range 4<X<7200")
}
timeoutAfter := time.After(time.Duration(timeOutAfterSeconds) * time.Second)
tick := time.NewTicker(3 * time.Second)
for {
select {
case <-timeoutAfter:
return fmt.Errorf("timed out waiting for VM guest customization status to exit state %s after %d seconds",
unwantedStatus, timeOutAfterSeconds)
case <-tick.C:
currentStatus, err := vm.GetGuestCustomizationStatus()
if err != nil {
return fmt.Errorf("could not get VM customization status %s", err)
}
if currentStatus != unwantedStatus {
return nil
}
}
}
}
// Customize function allows to set ComputerName, apply customization script and enable or disable the changeSid option
//
// Deprecated: Use vm.SetGuestCustomizationSection()
func (vm *VM) Customize(computerName, script string, changeSid bool) (Task, error) {
err := vm.Refresh()
if err != nil {
return Task{}, fmt.Errorf("error refreshing VM before running customization: %s", err)
}
vu := &types.GuestCustomizationSection{
Ovf: types.XMLNamespaceOVF,
Xsi: types.XMLNamespaceXSI,
Xmlns: types.XMLNamespaceVCloud,
HREF: vm.VM.HREF,
Type: types.MimeGuestCustomizationSection,
Info: "Specifies Guest OS Customization Settings",
Enabled: takeBoolPointer(true),
ComputerName: computerName,
CustomizationScript: script,
ChangeSid: takeBoolPointer(changeSid),
}
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/guestCustomizationSection/"
// Return the task
return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPut,
types.MimeGuestCustomizationSection, "error customizing VM: %s", vu)
}
// Undeploy triggers a VM undeploy and power off action. "Power off" action in UI behaves this way.
func (vm *VM) Undeploy() (Task, error) {
vu := &types.UndeployVAppParams{
Xmlns: types.XMLNamespaceVCloud,
UndeployPowerAction: "powerOff",
}
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/action/undeploy"
// Return the task
return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost,
types.MimeUndeployVappParams, "error undeploy VM: %s", vu)
}
// Attach or detach an independent disk
// Use the disk/action/attach or disk/action/detach links in a VM to attach or detach an independent disk.
// Reference: vCloud API Programming Guide for Service Providers vCloud API 30.0 PDF Page 164 - 165,
// https://vdc-download.vmware.com/vmwb-repository/dcr-public/1b6cf07d-adb3-4dba-8c47-9c1c92b04857/
// 241956dd-e128-4fcc-8131-bf66e1edd895/vcloud_sp_api_guide_30_0.pdf
func (vm *VM) attachOrDetachDisk(diskParams *types.DiskAttachOrDetachParams, rel string) (Task, error) {
util.Logger.Printf("[TRACE] Attach or detach disk, href: %s, rel: %s \n", diskParams.Disk.HREF, rel)
var attachOrDetachDiskLink *types.Link
for _, link := range vm.VM.Link {
if link.Rel == rel && link.Type == types.MimeDiskAttachOrDetachParams {
util.Logger.Printf("[TRACE] Attach or detach disk - found the proper link for request, HREF: %s, name: %s, type: %s, id: %s, rel: %s \n",
link.HREF,
link.Name,
link.Type,
link.ID,
link.Rel)
attachOrDetachDiskLink = link
}
}
if attachOrDetachDiskLink == nil {
return Task{}, fmt.Errorf("could not find request URL for attach or detach disk in disk Link")
}
diskParams.Xmlns = types.XMLNamespaceVCloud
// Return the task
return vm.client.ExecuteTaskRequest(attachOrDetachDiskLink.HREF, http.MethodPost,
attachOrDetachDiskLink.Type, "error attach or detach disk: %s", diskParams)
}
// AttachDisk attaches an independent disk
// Call attachOrDetachDisk with disk and types.RelDiskAttach to attach an independent disk.
// Please verify the independent disk is not connected to any VM before calling this function.
// If the independent disk is connected to a VM, the task will be failed.
// Reference: vCloud API Programming Guide for Service Providers vCloud API 30.0 PDF Page 164 - 165,
// https://vdc-download.vmware.com/vmwb-repository/dcr-public/1b6cf07d-adb3-4dba-8c47-9c1c92b04857/
// 241956dd-e128-4fcc-8131-bf66e1edd895/vcloud_sp_api_guide_30_0.pdf
func (vm *VM) AttachDisk(diskParams *types.DiskAttachOrDetachParams) (Task, error) {
if diskParams == nil || diskParams.Disk == nil || diskParams.Disk.HREF == "" {
return Task{}, fmt.Errorf("could not find disk info for attach")
}
util.Logger.Printf("[TRACE] Attach disk, HREF: %s\n", diskParams.Disk.HREF)
return vm.attachOrDetachDisk(diskParams, types.RelDiskAttach)
}
// DetachDisk detaches an independent disk
// Call attachOrDetachDisk with disk and types.RelDiskDetach to detach an independent disk.
// Please verify the independent disk is connected the VM before calling this function.
// If the independent disk is not connected to the VM, the task will be failed.
// Reference: vCloud API Programming Guide for Service Providers vCloud API 30.0 PDF Page 164 - 165,
// https://vdc-download.vmware.com/vmwb-repository/dcr-public/1b6cf07d-adb3-4dba-8c47-9c1c92b04857/
// 241956dd-e128-4fcc-8131-bf66e1edd895/vcloud_sp_api_guide_30_0.pdf
func (vm *VM) DetachDisk(diskParams *types.DiskAttachOrDetachParams) (Task, error) {
if diskParams == nil || diskParams.Disk == nil || diskParams.Disk.HREF == "" {
return Task{}, fmt.Errorf("could not find disk info for detach")
}
util.Logger.Printf("[TRACE] Detach disk, HREF: %s\n", diskParams.Disk.HREF)
return vm.attachOrDetachDisk(diskParams, types.RelDiskDetach)
}
// HandleInsertMedia helper function finds media and calls InsertMedia
func (vm *VM) HandleInsertMedia(org *Org, catalogName, mediaName string) (Task, error) {
catalog, err := org.GetCatalogByName(catalogName, false)
if err != nil {
return Task{}, err
}
media, err := catalog.GetMediaByName(mediaName, false)
if err != nil {
return Task{}, err
}
return vm.InsertMedia(&types.MediaInsertOrEjectParams{
Media: &types.Reference{
HREF: media.Media.HREF,
Name: media.Media.Name,
ID: media.Media.ID,
Type: media.Media.Type,
},
})
}
// HandleEjectMediaAndAnswer helper function which finds media, calls EjectMedia, waits for task to complete and answer question.
// Also waits until VM status refreshes - this added as 9.7-10.0 vCD versions has lag in status update.
// answerYes - handles question risen when VM is running. True value enforces ejection.
func (vm *VM) HandleEjectMediaAndAnswer(org *Org, catalogName, mediaName string, answerYes bool) (*VM, error) {
task, err := vm.HandleEjectMedia(org, catalogName, mediaName)
if err != nil {
return nil, fmt.Errorf("error: %s", err)
}
err = task.WaitTaskCompletion(answerYes)
if err != nil {
return nil, fmt.Errorf("error: %s", err)
}
for i := 0; i < 10; i++ {
err = vm.Refresh()
if err != nil {
return nil, fmt.Errorf("error: %s", err)
}
if !isMediaInjected(vm.VM.VirtualHardwareSection.Item) {
return vm, nil
}
time.Sleep(200 * time.Millisecond)
}
return nil, fmt.Errorf("eject media executed but waiting for state update failed")
}
// check resource subtype for specific value which means media is injected
func isMediaInjected(items []*types.VirtualHardwareItem) bool {
for _, hardwareItem := range items {
if hardwareItem.ResourceSubType == types.VMsCDResourceSubType {
return true
}
}
return false
}
// HandleEjectMedia is a helper function which finds media and calls EjectMedia
func (vm *VM) HandleEjectMedia(org *Org, catalogName, mediaName string) (EjectTask, error) {
catalog, err := org.GetCatalogByName(catalogName, false)
if err != nil {
return EjectTask{}, err
}
media, err := catalog.GetMediaByName(mediaName, false)
if err != nil {
return EjectTask{}, err
}
task, err := vm.EjectMedia(&types.MediaInsertOrEjectParams{
Media: &types.Reference{
HREF: media.Media.HREF,
},
})
return task, err
}
// InsertMedia insert media for a VM
// Call insertOrEjectMedia with media and types.RelMediaInsertMedia to insert media from VM.
func (vm *VM) InsertMedia(mediaParams *types.MediaInsertOrEjectParams) (Task, error) {
util.Logger.Printf("[TRACE] Insert media, HREF: %s\n", mediaParams.Media.HREF)
err := validateMediaParams(mediaParams)
if err != nil {
return Task{}, err
}
return vm.insertOrEjectMedia(mediaParams, types.RelMediaInsertMedia)
}
// EjectMedia ejects media from VM
// Call insertOrEjectMedia with media and types.RelMediaEjectMedia to eject media from VM.
// If media isn't inserted then task still will be successful.
func (vm *VM) EjectMedia(mediaParams *types.MediaInsertOrEjectParams) (EjectTask, error) {
util.Logger.Printf("[TRACE] Detach disk, HREF: %s\n", mediaParams.Media.HREF)
err := validateMediaParams(mediaParams)
if err != nil {
return EjectTask{}, err
}
task, err := vm.insertOrEjectMedia(mediaParams, types.RelMediaEjectMedia)
if err != nil {
return EjectTask{}, err
}
return *NewEjectTask(&task, vm), nil
}
// validates that media and media.href isn't empty
func validateMediaParams(mediaParams *types.MediaInsertOrEjectParams) error {
if mediaParams.Media == nil {
return fmt.Errorf("could not find media info for eject")
}
if mediaParams.Media.HREF == "" {
return fmt.Errorf("could not find media HREF which is required for insert")
}
return nil
}
// Insert or eject a media for VM
// Use the vm/action/insert or vm/action/eject links in a VM to insert or eject media.
// Reference:
// https://code.vmware.com/apis/287/vcloud#/doc/doc/operations/POST-InsertCdRom.html
// https://code.vmware.com/apis/287/vcloud#/doc/doc/operations/POST-EjectCdRom.html
func (vm *VM) insertOrEjectMedia(mediaParams *types.MediaInsertOrEjectParams, linkRel string) (Task, error) {
util.Logger.Printf("[TRACE] Insert or eject media, href: %s, name: %s, , linkRel: %s \n", mediaParams.Media.HREF, mediaParams.Media.Name, linkRel)
var insertOrEjectMediaLink *types.Link
for _, link := range vm.VM.Link {
if link.Rel == linkRel && link.Type == types.MimeMediaInsertOrEjectParams {
util.Logger.Printf("[TRACE] Insert or eject media - found the proper link for request, HREF: %s, "+
"name: %s, type: %s, id: %s, rel: %s \n", link.HREF, link.Name, link.Type, link.ID, link.Rel)
insertOrEjectMediaLink = link
}
}
if insertOrEjectMediaLink == nil {
return Task{}, fmt.Errorf("could not find request URL for insert or eject media")
}
mediaParams.Xmlns = types.XMLNamespaceVCloud
// Return the task
return vm.client.ExecuteTaskRequest(insertOrEjectMediaLink.HREF, http.MethodPost,
insertOrEjectMediaLink.Type, "error insert or eject media: %s", mediaParams)
}
// GetQuestion uses the get existing VM question for operation which need additional response
// Reference:
// https://code.vmware.com/apis/287/vcloud#/doc/doc/operations/GET-VmPendingQuestion.html
func (vm *VM) GetQuestion() (types.VmPendingQuestion, error) {
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/question"
req := vm.client.NewRequest(map[string]string{}, http.MethodGet, *apiEndpoint, nil)
resp, err := vm.client.Http.Do(req)
if err != nil {
return types.VmPendingQuestion{}, fmt.Errorf("error getting VM question: %s", err)
}
// vCD security feature - on no question return 403 access error
if http.StatusForbidden == resp.StatusCode {
util.Logger.Printf("No question found for VM: %s\n", vm.VM.ID)
return types.VmPendingQuestion{}, nil
}
if http.StatusOK != resp.StatusCode {
return types.VmPendingQuestion{}, fmt.Errorf("error getting question: %s", ParseErr(types.BodyTypeXML, resp, &types.Error{}))
}
question := &types.VmPendingQuestion{}
if err = decodeBody(types.BodyTypeXML, resp, question); err != nil {
return types.VmPendingQuestion{}, fmt.Errorf("error decoding question response: %s", err)
}
// The request was successful
return *question, nil
}
// AnswerQuestion uses the provided answer to existing VM question for operation which need additional response
// Reference:
// https://code.vmware.com/apis/287/vcloud#/doc/doc/operations/POST-AnswerVmPendingQuestion.html
func (vm *VM) AnswerQuestion(questionId string, choiceId int) error {
//validate input
if questionId == "" {
return fmt.Errorf("questionId can not be empty")
}
answer := &types.VmQuestionAnswer{
Xmlns: types.XMLNamespaceVCloud,
QuestionId: questionId,
ChoiceId: choiceId,
}
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/question/action/answer"
return vm.client.ExecuteRequestWithoutResponse(apiEndpoint.String(), http.MethodPost,
"", "error asnwering question: %s", answer)
}
// ToggleHardwareVirtualization allows to either enable or disable hardware assisted
// CPU virtualization for the VM. It can only be performed on a powered off VM and
// will return an error otherwise. This is mainly useful for hypervisor nesting.
func (vm *VM) ToggleHardwareVirtualization(isEnabled bool) (Task, error) {
vmStatus, err := vm.GetStatus()
if err != nil {
return Task{}, fmt.Errorf("unable to toggle hardware virtualization: %s", err)
}
if vmStatus != "POWERED_OFF" {
return Task{}, fmt.Errorf("hardware virtualization can be changed from powered off state, status: %s", vmStatus)
}
apiEndpoint := urlParseRequestURI(vm.VM.HREF)
if isEnabled {
apiEndpoint.Path += "/action/enableNestedHypervisor"
} else {
apiEndpoint.Path += "/action/disableNestedHypervisor"
}
errMessage := fmt.Sprintf("error toggling hypervisor nesting feature to %t for VM: %%s", isEnabled)
return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost,
"", errMessage, nil)
}
// SetProductSectionList sets product section for a VM. It allows to change VM guest properties.
//
// The slice of properties "ProductSectionList.ProductSection.Property" is not necessarily ordered
// or returned as set before
func (vm *VM) SetProductSectionList(productSection *types.ProductSectionList) (*types.ProductSectionList, error) {
err := setProductSectionList(vm.client, vm.VM.HREF, productSection)
if err != nil {
return nil, fmt.Errorf("unable to set VM product section: %s", err)
}
return vm.GetProductSectionList()
}
// GetProductSectionList retrieves product section for a VM. It allows to read VM guest properties.
//
// The slice of properties "ProductSectionList.ProductSection.Property" is not necessarily ordered
// or returned as set before
func (vm *VM) GetProductSectionList() (*types.ProductSectionList, error) {
return getProductSectionList(vm.client, vm.VM.HREF)
}
// GetGuestCustomizationSection retrieves guest customization section for a VM. It allows to read VM guest customization properties.
func (vm *VM) GetGuestCustomizationSection() (*types.GuestCustomizationSection, error) {
if vm == nil || vm.VM.HREF == "" {
return nil, fmt.Errorf("vm or href cannot be empty to get guest customization section")
}
guestCustomizationSection := &types.GuestCustomizationSection{}
_, err := vm.client.ExecuteRequest(vm.VM.HREF+"/guestCustomizationSection", http.MethodGet,
types.MimeGuestCustomizationSection, "error retrieving guest customization section : %s", nil, guestCustomizationSection)
if err != nil {
return nil, fmt.Errorf("unable to retrieve guest customization section: %s", err)
}
return guestCustomizationSection, nil
}
// SetGuestCustomizationSection sets guest customization section for a VM. It allows to change VM guest customization properties.
func (vm *VM) SetGuestCustomizationSection(guestCustomizationSection *types.GuestCustomizationSection) (*types.GuestCustomizationSection, error) {
if vm == nil || vm.VM.HREF == "" {
return nil, fmt.Errorf("vm or href cannot be empty to get guest customization section")
}
guestCustomizationSection.Xmlns = types.XMLNamespaceVCloud
guestCustomizationSection.Ovf = types.XMLNamespaceOVF
task, err := vm.client.ExecuteTaskRequest(vm.VM.HREF+"/guestCustomizationSection", http.MethodPut,
types.MimeGuestCustomizationSection, "error setting product section: %s", guestCustomizationSection)
if err != nil {
return nil, fmt.Errorf("unable to set guest customization section: %s", err)
}
err = task.WaitTaskCompletion()
if err != nil {
return nil, fmt.Errorf("task for setting guest customization section failed: %s", err)
}
return vm.GetGuestCustomizationSection()
}
// GetParentVApp find parent vApp for VM by checking its "up" "link".
//
// Note. The VM has a parent vApp defined even if it was created as a standalone
func (vm *VM) GetParentVApp() (*VApp, error) {
if vm == nil || vm.VM == nil {
return nil, fmt.Errorf("vm object cannot be nil to get parent vApp")
}
for _, link := range vm.VM.Link {
if link.Type == types.MimeVApp && link.Rel == "up" {
vapp := NewVApp(vm.client)
vapp.VApp.HREF = link.HREF
err := vapp.Refresh()
if err != nil {
return nil, fmt.Errorf("could not refresh parent vApp for VM %s: %s", vm.VM.Name, err)
}
return vapp, nil
}
}
return nil, fmt.Errorf("could not find parent vApp link")
}
// GetParentVdc returns parent VDC for VM
func (vm *VM) GetParentVdc() (*Vdc, error) {
if vm == nil || vm.VM == nil {
return nil, fmt.Errorf("vm object cannot be nil to get parent vApp")
}
vapp, err := vm.GetParentVApp()
if err != nil {
return nil, fmt.Errorf("could not find parent vApp for VM %s: %s", vm.VM.Name, err)
}
vdc, err := vapp.getParentVDC()
if err != nil {
return nil, fmt.Errorf("could not find parent vApp for VM %s: %s", vm.VM.Name, err)
}
return &vdc, nil
}
// getEdgeGatewaysForRoutedNics checks if any NICs are using routed networks and are attached to
// edge gateway
func (vm *VM) getEdgeGatewaysForRoutedNics(nicDhcpConfigs []nicDhcpConfig) ([]nicDhcpConfig, error) {
// Lookup parent vDC for VM
vdc, err := vm.GetParentVdc()
if err != nil {
return nil, fmt.Errorf("could not find parent vDC for VM %s: %s", vm.VM.Name, err)
}
for index, nic := range nicDhcpConfigs {
edgeGatewayName, err := vm.getEdgeGatewayNameForNic(nic.vmNicIndex)
if err != nil && !IsNotFound(err) {
return nil, fmt.Errorf("could not validate if NIC %d uses routed network attached to edge gateway: %s",
nic.vmNicIndex, err)
}
// This nicIndex is not attached to routed network, move further
if IsNotFound(err) {
util.Logger.Printf("[TRACE] [DHCP IP Lookup] VM '%s' NIC with index %d is not attached to edge gateway routed network\n",
vm.VM.Name, nic.vmNicIndex)
} else {
// Lookup edge gateway
edgeGateway, err := vdc.GetEdgeGatewayByName(edgeGatewayName, false)
if err != nil {
return nil, fmt.Errorf("could not lookup edge gateway for routed network on NIC %d: %s",
nic.vmNicIndex, err)
}
util.Logger.Printf("[TRACE] [DHCP IP Lookup] VM '%s' NIC with index %d is attached to edge gateway routed network\n",
vm.VM.Name, nic.vmNicIndex)
nicDhcpConfigs[index].routedNetworkEdgeGateway = edgeGateway
}
}
return nicDhcpConfigs, nil
}
// nicDhcpConfig is used to group data for carrying between multiple functions and optimizing on API
// calls
type nicDhcpConfig struct {
vmNicIndex int
ip string
mac string
routedNetworkEdgeGateway *EdgeGateway
}
// nicDhcpConfigs is a slice of nicDhcpConfig
type nicDhcpConfigs []nicDhcpConfig
// getIpsFromNicDhcpConfigs extracts just IP addresses from nicDhcpConfigs
func getIpsFromNicDhcpConfigs(nicConfigs []nicDhcpConfig) []string {
result := make([]string, len(nicConfigs))
for index, nicConfig := range nicConfigs {
result[index] = nicConfig.ip
}
return result
}
// allNicsHaveIps checks if all nicDhcpConfig in slice have not empty IP field
func allNicsHaveIps(nicConfigs []nicDhcpConfig) bool {
allNicsHaveIps := true
for _, nicConfig := range nicConfigs {
if nicConfig.ip == "" {
allNicsHaveIps = false
}
}
return allNicsHaveIps
}
// WaitForDhcpIpByNicIndexes accepts a slice of NIC indexes in VM, tries to get these IPs up to
// maxWaitSeconds and then returns:
// * a list of IPs
// * whether the function hit timeout (some IP values may be available after timeout)
// * error
//
// This function checks a slice of nicIndexes and reuses all possible API calls. It may return a
// partial result for IP addresses when the timeout is hit.
//
// Getting a DHCP address is complicated because vCD (in UI and in types.NetworkConnectionSection)
// reports IP addresses only when guest tools are present on a VM. This function also attempts to
// check if VM NICs are attached to routed network on edge gateway - then there is a chance that
// built-in DHCP pools are used and active DHCP leases can be found.
//
// For this function to work - at least one the following must be true:
// * VM has guest tools (vCD UI shows IP address). (Takes longer time)
// * VM DHCP interface is connected to routed Org network and is using NSX-V Edge Gateway DHCP. (Takes
// less time, but is more constrained)
func (vm *VM) WaitForDhcpIpByNicIndexes(nicIndexes []int, maxWaitSeconds int, useNsxvDhcpLeaseCheck bool) ([]string, bool, error) {
util.Logger.Printf("[TRACE] [DHCP IP Lookup] VM '%s' attempting to lookup IP addresses for DHCP NICs %v\n",
vm.VM.Name, nicIndexes)
// validate NIC indexes
if len(nicIndexes) == 0 {
return []string{}, false, fmt.Errorf("at least one NIC index must be specified")
}
for index, nicIndex := range nicIndexes {
if nicIndex < 0 {
return []string{}, false, fmt.Errorf("NIC index %d cannot be negative", index)
}
}