Skip to content

Commit b507229

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 dded055 commit b507229

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
@@ -1113,6 +1113,11 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg
11131113
* `0:0:0:0:0:0:0:0/0` - allow only ipv6 addresses
11141114
* `192.168.1.0/24` - only allow ipv4 addresses from 192.168.1.1 to 192.168.1.254
11151115

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

11181123

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

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

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

11521157

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
@@ -11,6 +11,7 @@ import (
1111
"log"
1212
"net"
1313
"reflect"
14+
"sort"
1415
"strconv"
1516
"strings"
1617
"time"
@@ -41,7 +42,7 @@ type VirtualMachine interface {
4142
Reconfigure(spec types.VirtualMachineConfigSpec) error
4243
Customize(spec types.CustomizationSpec) error
4344
ResizeDisk(diskSize int64) ([]types.BaseVirtualDeviceConfigSpec, error)
44-
WaitForIP(ctx context.Context, ipNet *net.IPNet) (string, error)
45+
WaitForIP(ctx context.Context, ipNet *net.IPNet, adapterIndex *int) (string, error)
4546
PowerOn() error
4647
PowerOff() error
4748
IsPoweredOff() (bool, error)
@@ -831,30 +832,136 @@ func (vm *VirtualMachineDriver) PowerOn() error {
831832
return err
832833
}
833834

834-
// WaitForIP waits for the virtual machine to get an IP address.
835-
func (vm *VirtualMachineDriver) WaitForIP(ctx context.Context, ipNet *net.IPNet) (string, error) {
836-
netIP, err := vm.vm.WaitForNetIP(ctx, false)
835+
// WaitForIP waits for the virtual machine to get an IP address on a guest network adapter.
836+
// When adapterIndex is nil, returns when any ethernet adapter has a matching address (not all adapters).
837+
// When adapterIndex is set, waits only on ethernet-{index}.
838+
func (vm *VirtualMachineDriver) WaitForIP(ctx context.Context, ipNet *net.IPNet, adapterIndex *int) (string, error) {
839+
if adapterIndex != nil {
840+
device := fmt.Sprintf("ethernet-%d", *adapterIndex)
841+
netIP, err := vm.vm.WaitForNetIP(ctx, false, device)
842+
if err != nil {
843+
return "", err
844+
}
845+
return selectIPFromNetIP(netIP, ipNet), nil
846+
}
847+
848+
return vm.waitForAnyNetIP(ctx, ipNet)
849+
}
850+
851+
func (vm *VirtualMachineDriver) waitForAnyNetIP(ctx context.Context, ipNet *net.IPNet) (string, error) {
852+
macs, err := vm.waitForEthernetMACs(ctx)
837853
if err != nil {
838854
return "", err
839855
}
840856

841-
for _, ips := range netIP {
842-
for _, ip := range ips {
843-
parseIP := net.ParseIP(ip)
844-
if ipNet != nil && !ipNet.Contains(parseIP) {
845-
// IP address is not in the expected range.
857+
p := property.DefaultCollector(vm.vm.Client())
858+
var ip string
859+
err = property.Wait(ctx, p, vm.vm.Reference(), []string{"guest.net"}, func(pc []types.PropertyChange) bool {
860+
netIP := guestNetIPFromPropertyChange(pc, macs, false)
861+
if candidate := selectIPFromNetIP(netIP, ipNet); candidate != "" {
862+
ip = candidate
863+
return true
864+
}
865+
return false
866+
})
867+
if err != nil {
868+
return "", err
869+
}
870+
871+
return ip, nil
872+
}
873+
874+
func (vm *VirtualMachineDriver) waitForEthernetMACs(ctx context.Context) (map[string][]string, error) {
875+
macs := make(map[string][]string)
876+
p := property.DefaultCollector(vm.vm.Client())
877+
878+
err := property.Wait(ctx, p, vm.vm.Reference(), []string{"config.hardware.device"}, func(pc []types.PropertyChange) bool {
879+
for _, c := range pc {
880+
if c.Op != types.PropertyChangeOpAssign {
846881
continue
847882
}
848-
// Default to IPv4 if no IPNet is provided.
849-
if ipNet == nil && parseIP.To4() == nil {
883+
884+
devices := object.VirtualDeviceList(c.Val.(types.ArrayOfVirtualDevice).VirtualDevice)
885+
for _, d := range devices {
886+
if nic, ok := d.(types.BaseVirtualEthernetCard); ok {
887+
mac := strings.ToLower(nic.GetVirtualEthernetCard().MacAddress)
888+
if mac == "" {
889+
return false
890+
}
891+
macs[mac] = nil
892+
}
893+
}
894+
}
895+
896+
return true
897+
})
898+
899+
return macs, err
900+
}
901+
902+
func guestNetIPFromPropertyChange(pc []types.PropertyChange, macs map[string][]string, v4 bool) map[string][]string {
903+
netIP := make(map[string][]string, len(macs))
904+
for mac := range macs {
905+
netIP[mac] = nil
906+
}
907+
908+
for _, c := range pc {
909+
if c.Op != types.PropertyChangeOpAssign {
910+
continue
911+
}
912+
913+
nics := c.Val.(types.ArrayOfGuestNicInfo).GuestNicInfo
914+
for _, nic := range nics {
915+
mac := strings.ToLower(nic.MacAddress)
916+
if mac == "" || nic.IpConfig == nil {
850917
continue
851918
}
852-
return ip, nil
919+
920+
for _, addr := range nic.IpConfig.IpAddress {
921+
if _, ok := macs[mac]; !ok {
922+
continue
923+
}
924+
if v4 && net.ParseIP(addr.IpAddress).To4() == nil {
925+
continue
926+
}
927+
netIP[mac] = append(netIP[mac], addr.IpAddress)
928+
}
853929
}
854930
}
855931

856-
// Unable to find an IP address.
857-
return "", nil
932+
return netIP
933+
}
934+
935+
func selectIPFromNetIP(netIP map[string][]string, ipNet *net.IPNet) string {
936+
keys := make([]string, 0, len(netIP))
937+
for mac := range netIP {
938+
keys = append(keys, mac)
939+
}
940+
sort.Strings(keys)
941+
942+
for _, mac := range keys {
943+
for _, ip := range netIP[mac] {
944+
if ipMatchesFilter(ip, ipNet) {
945+
return ip
946+
}
947+
}
948+
}
949+
950+
return ""
951+
}
952+
953+
func ipMatchesFilter(ip string, ipNet *net.IPNet) bool {
954+
parseIP := net.ParseIP(ip)
955+
if parseIP == nil {
956+
return false
957+
}
958+
if ipNet != nil && !ipNet.Contains(parseIP) {
959+
return false
960+
}
961+
if ipNet == nil && parseIP.To4() == nil {
962+
return false
963+
}
964+
return true
858965
}
859966

860967
// 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)