Skip to content

Commit 43029ac

Browse files
committed
fix: do not require all nics to have an ip
Add optional ip_wait_adapter_index on vsphere-iso and vsphere-clone for environments where multiple NICs may receive routable addresses. Signed-off-by: Ryan Johnson <ryan@tenthirtyam.org>
1 parent 36535c4 commit 43029ac

13 files changed

Lines changed: 260 additions & 20 deletions

File tree

.web-docs/components/builder/vsphere-clone/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,11 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg
11081108
* `0:0:0:0:0:0:0:0/0` - allow only ipv6 addresses
11091109
* `192.168.1.0/24` - only allow ipv4 addresses from 192.168.1.1 to 192.168.1.254
11101110

1111+
- `ip_wait_adapter_index` (\*int) - Wait for an IP on a specific network adapter, identified by its zero-based index.
1112+
For `vsphere-iso`, index `0` is the first `network_adapters` block (vSphere device `ethernet-0`).
1113+
For `vsphere-clone`, index follows the template's ethernet device order.
1114+
When unset, the build proceeds when any adapter has a matching IP (see `ip_wait_address`).
1115+
11111116
<!-- End of code generated from the comments of the WaitIpConfig struct in builder/vsphere/common/step_wait_for_ip.go; -->
11121117

11131118

.web-docs/components/builder/vsphere-iso/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,11 @@ JSON Example:
11911191
* `0:0:0:0:0:0:0:0/0` - allow only ipv6 addresses
11921192
* `192.168.1.0/24` - only allow ipv4 addresses from 192.168.1.1 to 192.168.1.254
11931193

1194+
- `ip_wait_adapter_index` (\*int) - Wait for an IP on a specific network adapter, identified by its zero-based index.
1195+
For `vsphere-iso`, index `0` is the first `network_adapters` block (vSphere device `ethernet-0`).
1196+
For `vsphere-clone`, index follows the template's ethernet device order.
1197+
When unset, the build proceeds when any adapter has a matching IP (see `ip_wait_address`).
1198+
11941199
<!-- End of code generated from the comments of the WaitIpConfig struct in builder/vsphere/common/step_wait_for_ip.go; -->
11951200

11961201

builder/vsphere/clone/config.hcl2spec.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builder/vsphere/common/step_wait_for_ip.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ type WaitIpConfig struct {
4040
// * `192.168.1.0/24` - only allow ipv4 addresses from 192.168.1.1 to 192.168.1.254
4141
WaitAddress *string `mapstructure:"ip_wait_address"`
4242
ipnet *net.IPNet
43+
// Wait for an IP on a specific network adapter, identified by its zero-based index.
44+
// For `vsphere-iso`, index `0` is the first `network_adapters` block (vSphere device `ethernet-0`).
45+
// For `vsphere-clone`, index follows the template's ethernet device order.
46+
// When unset, the build proceeds when any adapter has a matching IP (see `ip_wait_address`).
47+
AdapterIndex *int `mapstructure:"ip_wait_adapter_index"`
4348

4449
// WaitTimeout is a total timeout. If the virtual machine changes IP frequently, and does not settle down, wait
4550
// until the timeout expires.
@@ -71,6 +76,10 @@ func (c *WaitIpConfig) Prepare() []error {
7176
}
7277
}
7378

79+
if c.AdapterIndex != nil && *c.AdapterIndex < 0 {
80+
errs = append(errs, fmt.Errorf("ip_wait_adapter_index must be >= 0"))
81+
}
82+
7483
return errs
7584
}
7685

@@ -147,7 +156,7 @@ func doGetIp(vm *driver.VirtualMachineDriver, ctx context.Context, c *WaitIpConf
147156
interval = 1 * time.Second
148157
}
149158
loop:
150-
ip, err := vm.WaitForIP(ctx, c.ipnet)
159+
ip, err := vm.WaitForIP(ctx, c.ipnet, c.AdapterIndex)
151160
if err != nil {
152161
return "", err
153162
}

builder/vsphere/common/step_wait_for_ip.hcl2spec.go

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// © Broadcom. All Rights Reserved.
2+
// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
package common
6+
7+
import (
8+
"testing"
9+
)
10+
11+
func TestWaitIpConfig_Prepare_adapterIndex(t *testing.T) {
12+
neg := -1
13+
c := WaitIpConfig{AdapterIndex: &neg}
14+
errs := c.Prepare()
15+
if len(errs) != 1 {
16+
t.Fatalf("expected 1 error, got %v", errs)
17+
}
18+
19+
zero := 0
20+
c = WaitIpConfig{AdapterIndex: &zero}
21+
if errs := c.Prepare(); len(errs) != 0 {
22+
t.Fatalf("unexpected errors: %v", errs)
23+
}
24+
}

builder/vsphere/driver/vm.go

Lines changed: 121 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"log"
1212
"net"
13+
"sort"
1314
"strconv"
1415
"strings"
1516
"time"
@@ -40,7 +41,7 @@ type VirtualMachine interface {
4041
Reconfigure(spec types.VirtualMachineConfigSpec) error
4142
Customize(spec types.CustomizationSpec) error
4243
ResizeDisk(diskSize int64) ([]types.BaseVirtualDeviceConfigSpec, error)
43-
WaitForIP(ctx context.Context, ipNet *net.IPNet) (string, error)
44+
WaitForIP(ctx context.Context, ipNet *net.IPNet, adapterIndex *int) (string, error)
4445
PowerOn() error
4546
PowerOff() error
4647
IsPoweredOff() (bool, error)
@@ -803,30 +804,136 @@ func (vm *VirtualMachineDriver) PowerOn() error {
803804
return err
804805
}
805806

806-
// WaitForIP waits for the virtual machine to get an IP address.
807-
func (vm *VirtualMachineDriver) WaitForIP(ctx context.Context, ipNet *net.IPNet) (string, error) {
808-
netIP, err := vm.vm.WaitForNetIP(ctx, false)
807+
// WaitForIP waits for the virtual machine to get an IP address on a guest network adapter.
808+
// When adapterIndex is nil, returns when any ethernet adapter has a matching address (not all adapters).
809+
// When adapterIndex is set, waits only on ethernet-{index}.
810+
func (vm *VirtualMachineDriver) WaitForIP(ctx context.Context, ipNet *net.IPNet, adapterIndex *int) (string, error) {
811+
if adapterIndex != nil {
812+
device := fmt.Sprintf("ethernet-%d", *adapterIndex)
813+
netIP, err := vm.vm.WaitForNetIP(ctx, false, device)
814+
if err != nil {
815+
return "", err
816+
}
817+
return selectIPFromNetIP(netIP, ipNet), nil
818+
}
819+
820+
return vm.waitForAnyNetIP(ctx, ipNet)
821+
}
822+
823+
func (vm *VirtualMachineDriver) waitForAnyNetIP(ctx context.Context, ipNet *net.IPNet) (string, error) {
824+
macs, err := vm.waitForEthernetMACs(ctx)
809825
if err != nil {
810826
return "", err
811827
}
812828

813-
for _, ips := range netIP {
814-
for _, ip := range ips {
815-
parseIP := net.ParseIP(ip)
816-
if ipNet != nil && !ipNet.Contains(parseIP) {
817-
// IP address is not in the expected range.
829+
p := property.DefaultCollector(vm.vm.Client())
830+
var ip string
831+
err = property.Wait(ctx, p, vm.vm.Reference(), []string{"guest.net"}, func(pc []types.PropertyChange) bool {
832+
netIP := guestNetIPFromPropertyChange(pc, macs, false)
833+
if candidate := selectIPFromNetIP(netIP, ipNet); candidate != "" {
834+
ip = candidate
835+
return true
836+
}
837+
return false
838+
})
839+
if err != nil {
840+
return "", err
841+
}
842+
843+
return ip, nil
844+
}
845+
846+
func (vm *VirtualMachineDriver) waitForEthernetMACs(ctx context.Context) (map[string][]string, error) {
847+
macs := make(map[string][]string)
848+
p := property.DefaultCollector(vm.vm.Client())
849+
850+
err := property.Wait(ctx, p, vm.vm.Reference(), []string{"config.hardware.device"}, func(pc []types.PropertyChange) bool {
851+
for _, c := range pc {
852+
if c.Op != types.PropertyChangeOpAssign {
818853
continue
819854
}
820-
// Default to IPv4 if no IPNet is provided.
821-
if ipNet == nil && parseIP.To4() == nil {
855+
856+
devices := object.VirtualDeviceList(c.Val.(types.ArrayOfVirtualDevice).VirtualDevice)
857+
for _, d := range devices {
858+
if nic, ok := d.(types.BaseVirtualEthernetCard); ok {
859+
mac := strings.ToLower(nic.GetVirtualEthernetCard().MacAddress)
860+
if mac == "" {
861+
return false
862+
}
863+
macs[mac] = nil
864+
}
865+
}
866+
}
867+
868+
return true
869+
})
870+
871+
return macs, err
872+
}
873+
874+
func guestNetIPFromPropertyChange(pc []types.PropertyChange, macs map[string][]string, v4 bool) map[string][]string {
875+
netIP := make(map[string][]string, len(macs))
876+
for mac := range macs {
877+
netIP[mac] = nil
878+
}
879+
880+
for _, c := range pc {
881+
if c.Op != types.PropertyChangeOpAssign {
882+
continue
883+
}
884+
885+
nics := c.Val.(types.ArrayOfGuestNicInfo).GuestNicInfo
886+
for _, nic := range nics {
887+
mac := strings.ToLower(nic.MacAddress)
888+
if mac == "" || nic.IpConfig == nil {
822889
continue
823890
}
824-
return ip, nil
891+
892+
for _, addr := range nic.IpConfig.IpAddress {
893+
if _, ok := macs[mac]; !ok {
894+
continue
895+
}
896+
if v4 && net.ParseIP(addr.IpAddress).To4() == nil {
897+
continue
898+
}
899+
netIP[mac] = append(netIP[mac], addr.IpAddress)
900+
}
825901
}
826902
}
827903

828-
// Unable to find an IP address.
829-
return "", nil
904+
return netIP
905+
}
906+
907+
func selectIPFromNetIP(netIP map[string][]string, ipNet *net.IPNet) string {
908+
keys := make([]string, 0, len(netIP))
909+
for mac := range netIP {
910+
keys = append(keys, mac)
911+
}
912+
sort.Strings(keys)
913+
914+
for _, mac := range keys {
915+
for _, ip := range netIP[mac] {
916+
if ipMatchesFilter(ip, ipNet) {
917+
return ip
918+
}
919+
}
920+
}
921+
922+
return ""
923+
}
924+
925+
func ipMatchesFilter(ip string, ipNet *net.IPNet) bool {
926+
parseIP := net.ParseIP(ip)
927+
if parseIP == nil {
928+
return false
929+
}
930+
if ipNet != nil && !ipNet.Contains(parseIP) {
931+
return false
932+
}
933+
if ipNet == nil && parseIP.To4() == nil {
934+
return false
935+
}
936+
return true
830937
}
831938

832939
// PowerOff stops the virtual machine and waits for the operation to complete.

builder/vsphere/driver/vm_clone_acc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func startAndStopCheck(t *testing.T, vm VirtualMachine, config *CloneConfig) {
242242
stopper := startVM(t, vm, config.Name)
243243
defer stopper()
244244

245-
switch ip, err := vm.WaitForIP(context.TODO(), nil); {
245+
switch ip, err := vm.WaitForIP(context.TODO(), nil, nil); {
246246
case err != nil:
247247
t.Errorf("Cannot obtain IP address from created vm '%v': %v", config.Name, err)
248248
case net.ParseIP(ip) == nil:
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// © Broadcom. All Rights Reserved.
2+
// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
package driver
6+
7+
import (
8+
"net"
9+
"testing"
10+
)
11+
12+
func TestSelectIPFromNetIP(t *testing.T) {
13+
_, cidr, err := net.ParseCIDR("192.168.1.0/24")
14+
if err != nil {
15+
t.Fatal(err)
16+
}
17+
18+
tests := []struct {
19+
name string
20+
netIP map[string][]string
21+
ipNet *net.IPNet
22+
want string
23+
}{
24+
{
25+
name: "first matching mac in sort order",
26+
netIP: map[string][]string{
27+
"00:00:00:00:00:02": {"10.0.0.5"},
28+
"00:00:00:00:00:01": {"192.168.1.10"},
29+
},
30+
ipNet: cidr,
31+
want: "192.168.1.10",
32+
},
33+
{
34+
name: "skip nic without ip",
35+
netIP: map[string][]string{
36+
"00:00:00:00:00:01": nil,
37+
"00:00:00:00:00:02": {"192.168.1.20"},
38+
},
39+
ipNet: cidr,
40+
want: "192.168.1.20",
41+
},
42+
{
43+
name: "no match",
44+
netIP: map[string][]string{"00:00:00:00:00:01": {"10.0.0.1"}},
45+
ipNet: cidr,
46+
want: "",
47+
},
48+
}
49+
50+
for _, tt := range tests {
51+
t.Run(tt.name, func(t *testing.T) {
52+
if got := selectIPFromNetIP(tt.netIP, tt.ipNet); got != tt.want {
53+
t.Fatalf("selectIPFromNetIP() = %q, want %q", got, tt.want)
54+
}
55+
})
56+
}
57+
}
58+
59+
func TestIpMatchesFilter(t *testing.T) {
60+
_, cidr, _ := net.ParseCIDR("192.168.0.0/16")
61+
62+
if !ipMatchesFilter("192.168.1.1", cidr) {
63+
t.Fatal("expected match in cidr")
64+
}
65+
if ipMatchesFilter("10.0.0.1", cidr) {
66+
t.Fatal("expected no match outside cidr")
67+
}
68+
if !ipMatchesFilter("10.0.0.1", nil) {
69+
t.Fatal("expected ipv4 match with nil ipNet")
70+
}
71+
if ipMatchesFilter("2001:db8::1", nil) {
72+
t.Fatal("expected ipv6 skip with nil ipNet")
73+
}
74+
}

builder/vsphere/driver/vm_mock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func (vm *VirtualMachineMock) PowerOn() error {
155155
return nil
156156
}
157157

158-
func (vm *VirtualMachineMock) WaitForIP(ctx context.Context, ipNet *net.IPNet) (string, error) {
158+
func (vm *VirtualMachineMock) WaitForIP(ctx context.Context, ipNet *net.IPNet, adapterIndex *int) (string, error) {
159159
return "", nil
160160
}
161161

0 commit comments

Comments
 (0)