diff --git a/.web-docs/components/builder/vsphere-clone/README.md b/.web-docs/components/builder/vsphere-clone/README.md index 5cad5184..8e5ad5bd 100644 --- a/.web-docs/components/builder/vsphere-clone/README.md +++ b/.web-docs/components/builder/vsphere-clone/README.md @@ -353,8 +353,8 @@ disks are stored. - `disk_eagerly_scrub` (bool) - Enable eager scrubbing for the disk. Defaults to `false`. -- `disk_controller_index` (int) - The assigned disk controller for the disk. - Defaults to the first controller, `(0)`. +- `disk_controller_index` (int) - The assigned disk controller for the disk at the zero-based index + (0, 1, 2, ...). Defaults to the first controller, `(0)`. Mutually exclusive with `disk_controller_unit`. - `disk_controller_unit` (string) - Explicit controller address for the disk when cloning from a `template` @@ -1597,35 +1597,39 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg -- `ip_wait_timeout` (duration string | ex: "1h5m2s") - Amount of time to wait for VM's IP, similar to 'ssh_timeout'. - Defaults to `30m` (30 minutes). Refer to the Golang +- `ip_wait_timeout` (duration string | ex: "1h5m2s") - The amount of time to wait for virtual machine's IP, similar to + `ssh_timeout`. Defaults to `30m` (30 minutes). Refer to the Golang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) - documentation for full details. + documentation for more information. -- `ip_settle_timeout` (duration string | ex: "1h5m2s") - Amount of time to wait for VM's IP to settle down, sometimes VM may - report incorrect IP initially, then it is recommended to set that - parameter to apx. 2 minutes. Examples `45s` and `10m`. - Defaults to `5s` (5 seconds). Refer to the Golang - [ParseDuration](https://golang.org/pkg/time/#ParseDuration) - documentation for full details. +- `ip_settle_timeout` (duration string | ex: "1h5m2s") - The amount of time to wait for virtual machine's IP to settle down. For + example, `45s` and `10m`. Defaults to `5s` (5 seconds). Refer to the + Golang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) + documentation for more information. -- `ip_wait_address` (\*string) - Set this to a CIDR address to cause the service to wait for an address that is contained in - this network range. Defaults to `0.0.0.0/0` for any IPv4 address. +- `ip_wait_address` (\*string) - The IP address range in CIDR notation to wait for. Defaults to + `0.0.0.0/0` for any IPv4 address. - -> **Note:** This only filters which guest-reported IP is accepted; it does not disable IP wait. Use `disable_ip_wait` to skip - waiting for a guest-reported IP entirely. When `disable_ip_wait` is true, this setting still applies to HTTP IP discovery. + -> **Note:** This only filters which guest-reported IP is accepted; it + does not disable IP wait. Use `disable_ip_wait` to skip waiting for a + guest-reported IP entirely. When `disable_ip_wait` is `true`, this + setting still applies to HTTP IP discovery. Examples include: * empty string ("") - remove all filters - * `0:0:0:0:0:0:0:0/0` - allow only ipv6 addresses - * `192.168.1.0/24` - only allow ipv4 addresses from 192.168.1.1 to 192.168.1.254 + * `0:0:0:0:0:0:0:0/0` - allow only IPv6 addresses + * `192.168.1.0/24` - only allow IPv4 addresses from 192.168.1.1 to 192.168.1.254 + +- `ip_wait_adapter_index` (\*int) - When set, wait for an IP on a specific network adapter at this zero-based + index (0, 1, 2, ...). -- `disable_ip_wait` (bool) - When true, skip waiting for a guest-reported IP from vCenter. The default wait relies on - VMware Tools or open-vm-tools guest information. Use when they cannot be installed during - guest operating system install. Defaults to `false`. +- `disable_ip_wait` (bool) - When `true`, skip waiting for a guest-reported IP from vCenter. The + default wait relies on VMware Tools or open-vm-tools guest information. + Use when they cannot be installed during guest operating system install. + Defaults to `false`. - -> **Note:** You must set `ssh_host` or `winrm_host`; reachability timing uses `ssh_timeout` - or `winrm_timeout`, not `ip_wait_timeout`. + -> **Note:** You must set `ssh_host` or `winrm_host`; reachability timing + uses `ssh_timeout` or `winrm_timeout`, not `ip_wait_timeout`. diff --git a/.web-docs/components/builder/vsphere-iso/README.md b/.web-docs/components/builder/vsphere-iso/README.md index 96830f6d..96a310b4 100644 --- a/.web-docs/components/builder/vsphere-iso/README.md +++ b/.web-docs/components/builder/vsphere-iso/README.md @@ -877,8 +877,8 @@ JSON Example: - `disk_eagerly_scrub` (bool) - Enable eager scrubbing for the disk. Defaults to `false`. -- `disk_controller_index` (int) - The assigned disk controller for the disk. - Defaults to the first controller, `(0)`. +- `disk_controller_index` (int) - The assigned disk controller for the disk at the zero-based index + (0, 1, 2, ...). Defaults to the first controller, `(0)`. Mutually exclusive with `disk_controller_unit`. - `disk_controller_unit` (string) - Explicit controller address for the disk when cloning from a `template` @@ -1212,35 +1212,39 @@ JSON Example: -- `ip_wait_timeout` (duration string | ex: "1h5m2s") - Amount of time to wait for VM's IP, similar to 'ssh_timeout'. - Defaults to `30m` (30 minutes). Refer to the Golang +- `ip_wait_timeout` (duration string | ex: "1h5m2s") - The amount of time to wait for virtual machine's IP, similar to + `ssh_timeout`. Defaults to `30m` (30 minutes). Refer to the Golang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) - documentation for full details. + documentation for more information. -- `ip_settle_timeout` (duration string | ex: "1h5m2s") - Amount of time to wait for VM's IP to settle down, sometimes VM may - report incorrect IP initially, then it is recommended to set that - parameter to apx. 2 minutes. Examples `45s` and `10m`. - Defaults to `5s` (5 seconds). Refer to the Golang - [ParseDuration](https://golang.org/pkg/time/#ParseDuration) - documentation for full details. +- `ip_settle_timeout` (duration string | ex: "1h5m2s") - The amount of time to wait for virtual machine's IP to settle down. For + example, `45s` and `10m`. Defaults to `5s` (5 seconds). Refer to the + Golang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) + documentation for more information. -- `ip_wait_address` (\*string) - Set this to a CIDR address to cause the service to wait for an address that is contained in - this network range. Defaults to `0.0.0.0/0` for any IPv4 address. +- `ip_wait_address` (\*string) - The IP address range in CIDR notation to wait for. Defaults to + `0.0.0.0/0` for any IPv4 address. - -> **Note:** This only filters which guest-reported IP is accepted; it does not disable IP wait. Use `disable_ip_wait` to skip - waiting for a guest-reported IP entirely. When `disable_ip_wait` is true, this setting still applies to HTTP IP discovery. + -> **Note:** This only filters which guest-reported IP is accepted; it + does not disable IP wait. Use `disable_ip_wait` to skip waiting for a + guest-reported IP entirely. When `disable_ip_wait` is `true`, this + setting still applies to HTTP IP discovery. Examples include: * empty string ("") - remove all filters - * `0:0:0:0:0:0:0:0/0` - allow only ipv6 addresses - * `192.168.1.0/24` - only allow ipv4 addresses from 192.168.1.1 to 192.168.1.254 + * `0:0:0:0:0:0:0:0/0` - allow only IPv6 addresses + * `192.168.1.0/24` - only allow IPv4 addresses from 192.168.1.1 to 192.168.1.254 + +- `ip_wait_adapter_index` (\*int) - When set, wait for an IP on a specific network adapter at this zero-based + index (0, 1, 2, ...). -- `disable_ip_wait` (bool) - When true, skip waiting for a guest-reported IP from vCenter. The default wait relies on - VMware Tools or open-vm-tools guest information. Use when they cannot be installed during - guest operating system install. Defaults to `false`. +- `disable_ip_wait` (bool) - When `true`, skip waiting for a guest-reported IP from vCenter. The + default wait relies on VMware Tools or open-vm-tools guest information. + Use when they cannot be installed during guest operating system install. + Defaults to `false`. - -> **Note:** You must set `ssh_host` or `winrm_host`; reachability timing uses `ssh_timeout` - or `winrm_timeout`, not `ip_wait_timeout`. + -> **Note:** You must set `ssh_host` or `winrm_host`; reachability timing + uses `ssh_timeout` or `winrm_timeout`, not `ip_wait_timeout`. diff --git a/builder/vsphere/clone/config.hcl2spec.go b/builder/vsphere/clone/config.hcl2spec.go index 6113b285..679978ae 100644 --- a/builder/vsphere/clone/config.hcl2spec.go +++ b/builder/vsphere/clone/config.hcl2spec.go @@ -98,6 +98,7 @@ type FlatConfig struct { WaitTimeout *string `mapstructure:"ip_wait_timeout" cty:"ip_wait_timeout" hcl:"ip_wait_timeout"` SettleTimeout *string `mapstructure:"ip_settle_timeout" cty:"ip_settle_timeout" hcl:"ip_settle_timeout"` WaitAddress *string `mapstructure:"ip_wait_address" cty:"ip_wait_address" hcl:"ip_wait_address"` + WaitAdapterIndex *int `mapstructure:"ip_wait_adapter_index" cty:"ip_wait_adapter_index" hcl:"ip_wait_adapter_index"` DisableIpWait *bool `mapstructure:"disable_ip_wait" cty:"disable_ip_wait" hcl:"disable_ip_wait"` Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"` PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"` @@ -260,6 +261,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "ip_wait_timeout": &hcldec.AttrSpec{Name: "ip_wait_timeout", Type: cty.String, Required: false}, "ip_settle_timeout": &hcldec.AttrSpec{Name: "ip_settle_timeout", Type: cty.String, Required: false}, "ip_wait_address": &hcldec.AttrSpec{Name: "ip_wait_address", Type: cty.String, Required: false}, + "ip_wait_adapter_index": &hcldec.AttrSpec{Name: "ip_wait_adapter_index", Type: cty.Number, Required: false}, "disable_ip_wait": &hcldec.AttrSpec{Name: "disable_ip_wait", Type: cty.Bool, Required: false}, "communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false}, "pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false}, diff --git a/builder/vsphere/common/step_wait_for_ip.go b/builder/vsphere/common/step_wait_for_ip.go index e9cc3832..978f72e6 100644 --- a/builder/vsphere/common/step_wait_for_ip.go +++ b/builder/vsphere/common/step_wait_for_ip.go @@ -20,36 +20,40 @@ import ( ) type WaitIpConfig struct { - // Amount of time to wait for VM's IP, similar to 'ssh_timeout'. - // Defaults to `30m` (30 minutes). Refer to the Golang + // The amount of time to wait for virtual machine's IP, similar to + // `ssh_timeout`. Defaults to `30m` (30 minutes). Refer to the Golang // [ParseDuration](https://golang.org/pkg/time/#ParseDuration) - // documentation for full details. + // documentation for more information. WaitTimeout time.Duration `mapstructure:"ip_wait_timeout"` - // Amount of time to wait for VM's IP to settle down, sometimes VM may - // report incorrect IP initially, then it is recommended to set that - // parameter to apx. 2 minutes. Examples `45s` and `10m`. - // Defaults to `5s` (5 seconds). Refer to the Golang - // [ParseDuration](https://golang.org/pkg/time/#ParseDuration) - // documentation for full details. + // The amount of time to wait for virtual machine's IP to settle down. For + // example, `45s` and `10m`. Defaults to `5s` (5 seconds). Refer to the + // Golang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) + // documentation for more information. SettleTimeout time.Duration `mapstructure:"ip_settle_timeout"` - // Set this to a CIDR address to cause the service to wait for an address that is contained in - // this network range. Defaults to `0.0.0.0/0` for any IPv4 address. + // The IP address range in CIDR notation to wait for. Defaults to + // `0.0.0.0/0` for any IPv4 address. // - // -> **Note:** This only filters which guest-reported IP is accepted; it does not disable IP wait. Use `disable_ip_wait` to skip - // waiting for a guest-reported IP entirely. When `disable_ip_wait` is true, this setting still applies to HTTP IP discovery. + // -> **Note:** This only filters which guest-reported IP is accepted; it + // does not disable IP wait. Use `disable_ip_wait` to skip waiting for a + // guest-reported IP entirely. When `disable_ip_wait` is `true`, this + // setting still applies to HTTP IP discovery. // // Examples include: // * empty string ("") - remove all filters - // * `0:0:0:0:0:0:0:0/0` - allow only ipv6 addresses - // * `192.168.1.0/24` - only allow ipv4 addresses from 192.168.1.1 to 192.168.1.254 + // * `0:0:0:0:0:0:0:0/0` - allow only IPv6 addresses + // * `192.168.1.0/24` - only allow IPv4 addresses from 192.168.1.1 to 192.168.1.254 WaitAddress *string `mapstructure:"ip_wait_address"` ipnet *net.IPNet - // When true, skip waiting for a guest-reported IP from vCenter. The default wait relies on - // VMware Tools or open-vm-tools guest information. Use when they cannot be installed during - // guest operating system install. Defaults to `false`. + // When set, wait for an IP on a specific network adapter at this zero-based + // index (0, 1, 2, ...). + WaitAdapterIndex *int `mapstructure:"ip_wait_adapter_index"` + // When `true`, skip waiting for a guest-reported IP from vCenter. The + // default wait relies on VMware Tools or open-vm-tools guest information. + // Use when they cannot be installed during guest operating system install. + // Defaults to `false`. // - // -> **Note:** You must set `ssh_host` or `winrm_host`; reachability timing uses `ssh_timeout` - // or `winrm_timeout`, not `ip_wait_timeout`. + // -> **Note:** You must set `ssh_host` or `winrm_host`; reachability timing + // uses `ssh_timeout` or `winrm_timeout`, not `ip_wait_timeout`. DisableIpWait bool `mapstructure:"disable_ip_wait"` } @@ -79,6 +83,10 @@ func (c *WaitIpConfig) Prepare() []error { } } + if c.WaitAdapterIndex != nil && *c.WaitAdapterIndex < 0 { + errs = append(errs, fmt.Errorf("ip_wait_adapter_index must be >= 0")) + } + return errs } @@ -115,7 +123,11 @@ func (s *StepWaitForIp) Run(ctx context.Context, state multistep.StateBag) multi }() go func() { - ui.Say("Waiting for IP...") + if s.Config.WaitAdapterIndex != nil { + ui.Sayf("Waiting for IP on network adapter with index %d...", *s.Config.WaitAdapterIndex) + } else { + ui.Say("Waiting for IP...") + } ip, err = doGetIp(vm, sub, s.Config) waitDone <- true }() @@ -170,7 +182,7 @@ func doGetIp(vm *driver.VirtualMachineDriver, ctx context.Context, c *WaitIpConf interval = 1 * time.Second } loop: - ip, err := vm.WaitForIP(ctx, c.ipnet) + ip, err := vm.WaitForIP(ctx, c.ipnet, c.WaitAdapterIndex) if err != nil { return "", err } @@ -182,6 +194,15 @@ loop: default: } + if ip == "" { + select { + case <-ctx.Done(): + return "", fmt.Errorf("IP wait cancelled") + case <-time.After(interval): + goto loop + } + } + if prevIp == "" || prevIp != ip { if prevIp == "" { log.Printf("VM IP acquired: %s", ip) diff --git a/builder/vsphere/common/step_wait_for_ip.hcl2spec.go b/builder/vsphere/common/step_wait_for_ip.hcl2spec.go index 6d89bb60..b898bb19 100644 --- a/builder/vsphere/common/step_wait_for_ip.hcl2spec.go +++ b/builder/vsphere/common/step_wait_for_ip.hcl2spec.go @@ -10,10 +10,11 @@ import ( // FlatWaitIpConfig is an auto-generated flat version of WaitIpConfig. // Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. type FlatWaitIpConfig struct { - WaitTimeout *string `mapstructure:"ip_wait_timeout" cty:"ip_wait_timeout" hcl:"ip_wait_timeout"` - SettleTimeout *string `mapstructure:"ip_settle_timeout" cty:"ip_settle_timeout" hcl:"ip_settle_timeout"` - WaitAddress *string `mapstructure:"ip_wait_address" cty:"ip_wait_address" hcl:"ip_wait_address"` - DisableIpWait *bool `mapstructure:"disable_ip_wait" cty:"disable_ip_wait" hcl:"disable_ip_wait"` + WaitTimeout *string `mapstructure:"ip_wait_timeout" cty:"ip_wait_timeout" hcl:"ip_wait_timeout"` + SettleTimeout *string `mapstructure:"ip_settle_timeout" cty:"ip_settle_timeout" hcl:"ip_settle_timeout"` + WaitAddress *string `mapstructure:"ip_wait_address" cty:"ip_wait_address" hcl:"ip_wait_address"` + WaitAdapterIndex *int `mapstructure:"ip_wait_adapter_index" cty:"ip_wait_adapter_index" hcl:"ip_wait_adapter_index"` + DisableIpWait *bool `mapstructure:"disable_ip_wait" cty:"disable_ip_wait" hcl:"disable_ip_wait"` } // FlatMapstructure returns a new FlatWaitIpConfig. @@ -28,10 +29,11 @@ func (*WaitIpConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec. // The decoded values from this spec will then be applied to a FlatWaitIpConfig. func (*FlatWaitIpConfig) HCL2Spec() map[string]hcldec.Spec { s := map[string]hcldec.Spec{ - "ip_wait_timeout": &hcldec.AttrSpec{Name: "ip_wait_timeout", Type: cty.String, Required: false}, - "ip_settle_timeout": &hcldec.AttrSpec{Name: "ip_settle_timeout", Type: cty.String, Required: false}, - "ip_wait_address": &hcldec.AttrSpec{Name: "ip_wait_address", Type: cty.String, Required: false}, - "disable_ip_wait": &hcldec.AttrSpec{Name: "disable_ip_wait", Type: cty.Bool, Required: false}, + "ip_wait_timeout": &hcldec.AttrSpec{Name: "ip_wait_timeout", Type: cty.String, Required: false}, + "ip_settle_timeout": &hcldec.AttrSpec{Name: "ip_settle_timeout", Type: cty.String, Required: false}, + "ip_wait_address": &hcldec.AttrSpec{Name: "ip_wait_address", Type: cty.String, Required: false}, + "ip_wait_adapter_index": &hcldec.AttrSpec{Name: "ip_wait_adapter_index", Type: cty.Number, Required: false}, + "disable_ip_wait": &hcldec.AttrSpec{Name: "disable_ip_wait", Type: cty.Bool, Required: false}, } return s } diff --git a/builder/vsphere/common/step_wait_for_ip_test.go b/builder/vsphere/common/step_wait_for_ip_test.go index 9665ee5d..7d632563 100644 --- a/builder/vsphere/common/step_wait_for_ip_test.go +++ b/builder/vsphere/common/step_wait_for_ip_test.go @@ -62,3 +62,18 @@ func TestWaitIpConfig_ValidateDisableIpWait(t *testing.T) { } }) } + +func TestWaitIpConfig_Prepare_adapterIndex(t *testing.T) { + neg := -1 + c := WaitIpConfig{WaitAdapterIndex: &neg} + errs := c.Prepare() + if len(errs) != 1 { + t.Fatalf("expected 1 error, got %v", errs) + } + + zero := 0 + c = WaitIpConfig{WaitAdapterIndex: &zero} + if errs := c.Prepare(); len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } +} diff --git a/builder/vsphere/common/storage_config.go b/builder/vsphere/common/storage_config.go index 4e3bc6ce..5628f8c3 100644 --- a/builder/vsphere/common/storage_config.go +++ b/builder/vsphere/common/storage_config.go @@ -20,8 +20,8 @@ type DiskConfig struct { // Enable eager scrubbing for the disk. // Defaults to `false`. DiskEagerlyScrub bool `mapstructure:"disk_eagerly_scrub"` - // The assigned disk controller for the disk. - // Defaults to the first controller, `(0)`. + // The assigned disk controller for the disk at the zero-based index + // (0, 1, 2, ...). Defaults to the first controller, `(0)`. // Mutually exclusive with `disk_controller_unit`. DiskControllerIndex int `mapstructure:"disk_controller_index"` // Explicit controller address for the disk when cloning from a `template` diff --git a/builder/vsphere/driver/vm.go b/builder/vsphere/driver/vm.go index 0208b479..3d436034 100644 --- a/builder/vsphere/driver/vm.go +++ b/builder/vsphere/driver/vm.go @@ -10,6 +10,7 @@ import ( "fmt" "log" "net" + "sort" "strconv" "strings" "time" @@ -41,7 +42,7 @@ type VirtualMachine interface { Reconfigure(spec types.VirtualMachineConfigSpec) error Customize(spec types.CustomizationSpec) error ResizeDisk(diskSize int64) ([]types.BaseVirtualDeviceConfigSpec, error) - WaitForIP(ctx context.Context, ipNet *net.IPNet) (string, error) + WaitForIP(ctx context.Context, ipNet *net.IPNet, adapterIndex *int) (string, error) PowerOn() error PowerOff() error IsPoweredOff() (bool, error) @@ -821,30 +822,172 @@ func (vm *VirtualMachineDriver) PowerOn() error { return err } -// WaitForIP waits for the virtual machine to get an IP address. -func (vm *VirtualMachineDriver) WaitForIP(ctx context.Context, ipNet *net.IPNet) (string, error) { - netIP, err := vm.vm.WaitForNetIP(ctx, false) +// WaitForIP waits for a matching IP on a network adapter. +func (vm *VirtualMachineDriver) WaitForIP(ctx context.Context, ipNet *net.IPNet, adapterIndex *int) (string, error) { + adapters, err := vm.waitForEthernetAdapters(ctx) + if err != nil { + return "", err + } + if len(adapters) == 0 { + return "", fmt.Errorf("no ethernet adapters found") + } + + if adapterIndex != nil { + if *adapterIndex >= len(adapters) { + return "", fmt.Errorf("ip_wait_adapter_index %d is out of range for %d ethernet adapters", *adapterIndex, len(adapters)) + } + adapters = []ethernetAdapter{adapters[*adapterIndex]} + log.Printf("[INFO] waiting for IP on %s (index %d, mac %s)", adapters[0].name, *adapterIndex, adapters[0].mac) + } + + return vm.waitForMatchingNetIP(ctx, ipNet, adapters) +} + +type ethernetAdapter struct { + name string + mac string + unit int32 + key int32 +} + +func (vm *VirtualMachineDriver) waitForMatchingNetIP(ctx context.Context, ipNet *net.IPNet, adapters []ethernetAdapter) (string, error) { + p := property.DefaultCollector(vm.vm.Client()) + var ip string + err := property.Wait(ctx, p, vm.vm.Reference(), []string{"guest.net"}, func(pc []types.PropertyChange) bool { + nics := guestNicsFromPropertyChange(pc) + if candidate := selectIPByHardwareAdapters(adapters, nics, ipNet); candidate != "" { + ip = candidate + return true + } + return false + }) if err != nil { return "", err } - for _, ips := range netIP { - for _, ip := range ips { - parseIP := net.ParseIP(ip) - if ipNet != nil && !ipNet.Contains(parseIP) { - // IP address is not in the expected range. + return ip, nil +} + +func (vm *VirtualMachineDriver) waitForEthernetAdapters(ctx context.Context) ([]ethernetAdapter, error) { + p := property.DefaultCollector(vm.vm.Client()) + var adapters []ethernetAdapter + + err := property.Wait(ctx, p, vm.vm.Reference(), []string{"config.hardware.device"}, func(pc []types.PropertyChange) bool { + found := false + for _, c := range pc { + if c.Op != types.PropertyChangeOpAssign { continue } - // Default to IPv4 if no IPNet is provided. - if ipNet == nil && parseIP.To4() == nil { + + devices := object.VirtualDeviceList(c.Val.(types.ArrayOfVirtualDevice).VirtualDevice) + nics := ethernetAdaptersFromDevices(devices) + if len(nics) == 0 { + return false + } + for _, nic := range nics { + if nic.mac == "" { + return false + } + } + adapters = nics + found = true + } + return found + }) + if err != nil { + return nil, err + } + + return adapters, nil +} + +func ethernetAdaptersFromDevices(devices object.VirtualDeviceList) []ethernetAdapter { + var adapters []ethernetAdapter + for _, d := range devices { + nic, ok := d.(types.BaseVirtualEthernetCard) + if !ok { + continue + } + card := nic.GetVirtualEthernetCard() + unit := int32(0) + if card.UnitNumber != nil { + unit = *card.UnitNumber + } + adapters = append(adapters, ethernetAdapter{ + name: devices.Name(d), + mac: strings.ToLower(card.MacAddress), + unit: unit, + key: card.Key, + }) + } + sort.Slice(adapters, func(i, j int) bool { + if adapters[i].unit != adapters[j].unit { + return adapters[i].unit < adapters[j].unit + } + return adapters[i].key < adapters[j].key + }) + return adapters +} + +func guestNicsFromPropertyChange(pc []types.PropertyChange) []types.GuestNicInfo { + var nics []types.GuestNicInfo + for _, c := range pc { + if c.Op != types.PropertyChangeOpAssign { + continue + } + nics = append(nics, c.Val.(types.ArrayOfGuestNicInfo).GuestNicInfo...) + } + return nics +} + +func selectIPByHardwareAdapters(adapters []ethernetAdapter, nics []types.GuestNicInfo, ipNet *net.IPNet) string { + ipsByMAC := make(map[string][]string, len(nics)) + v4 := ipNetWantsIPv4(ipNet) + for _, nic := range nics { + mac := strings.ToLower(nic.MacAddress) + if mac == "" || nic.IpConfig == nil { + continue + } + for _, addr := range nic.IpConfig.IpAddress { + if v4 && net.ParseIP(addr.IpAddress).To4() == nil { continue } - return ip, nil + ipsByMAC[mac] = append(ipsByMAC[mac], addr.IpAddress) } } - // Unable to find an IP address. - return "", nil + for _, adapter := range adapters { + for _, ip := range ipsByMAC[adapter.mac] { + if ipMatchesFilter(ip, ipNet) { + return ip + } + } + } + return "" +} + +func ipMatchesFilter(ip string, ipNet *net.IPNet) bool { + parseIP := net.ParseIP(ip) + if parseIP == nil { + return false + } + if ipNet != nil && !ipNet.Contains(parseIP) { + return false + } + if ipNet == nil && parseIP.To4() == nil { + return false + } + return true +} + +// ipNetWantsIPv4 reports whether the wait filter should ignore non-IPv4 addresses +// while waiting. Nil and IPv4 CIDRs (including the default 0.0.0.0/0) wait for IPv4 +// so IPv6 link-local addresses do not complete the wait. +func ipNetWantsIPv4(ipNet *net.IPNet) bool { + if ipNet == nil { + return true + } + return ipNet.IP.To4() != nil } // PowerOff stops the virtual machine and waits for the operation to complete. diff --git a/builder/vsphere/driver/vm_clone_acc_test.go b/builder/vsphere/driver/vm_clone_acc_test.go index 0ef3ba39..7244c788 100644 --- a/builder/vsphere/driver/vm_clone_acc_test.go +++ b/builder/vsphere/driver/vm_clone_acc_test.go @@ -242,7 +242,7 @@ func startAndStopCheck(t *testing.T, vm VirtualMachine, config *CloneConfig) { stopper := startVM(t, vm, config.Name) defer stopper() - switch ip, err := vm.WaitForIP(context.Background(), nil); { + switch ip, err := vm.WaitForIP(context.Background(), nil, nil); { case err != nil: t.Errorf("Cannot obtain IP address from created vm '%v': %v", config.Name, err) case net.ParseIP(ip) == nil: diff --git a/builder/vsphere/driver/vm_ip_test.go b/builder/vsphere/driver/vm_ip_test.go new file mode 100644 index 00000000..0a7c636d --- /dev/null +++ b/builder/vsphere/driver/vm_ip_test.go @@ -0,0 +1,113 @@ +// © Broadcom. All Rights Reserved. +// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: MPL-2.0 + +package driver + +import ( + "net" + "testing" + + "github.com/vmware/govmomi/vim25/types" +) + +func guestNic(mac, ip string) types.GuestNicInfo { + nic := types.GuestNicInfo{MacAddress: mac} + if ip != "" { + nic.IpConfig = &types.NetIpConfigInfo{ + IpAddress: []types.NetIpConfigInfoIpAddress{{IpAddress: ip}}, + } + } + return nic +} + +func TestSelectIPByHardwareAdapters(t *testing.T) { + _, cidr, err := net.ParseCIDR("192.168.86.0/24") + if err != nil { + t.Fatal(err) + } + + adapters := []ethernetAdapter{ + {name: "ethernet-0", mac: "00:50:56:81:9a:a5", unit: 7}, + {name: "ethernet-1", mac: "00:50:56:81:01:d1", unit: 8}, + {name: "ethernet-2", mac: "00:50:56:81:3e:82", unit: 9}, + {name: "ethernet-3", mac: "00:50:56:81:71:a0", unit: 10}, + } + nics := []types.GuestNicInfo{ + guestNic("00:50:56:81:9a:a5", "192.168.86.201"), + guestNic("00:50:56:81:01:d1", "192.168.86.202"), + guestNic("00:50:56:81:71:a0", "192.168.86.203"), + guestNic("00:50:56:81:3e:82", "192.168.86.204"), + } + + if got := selectIPByHardwareAdapters(adapters, nics, cidr); got != "192.168.86.201" { + t.Fatalf("any adapter = %q, want 192.168.86.201", got) + } + if got := selectIPByHardwareAdapters(adapters[2:3], nics, cidr); got != "192.168.86.204" { + t.Fatalf("index 2 = %q, want 192.168.86.204 (MAC of ethernet-2)", got) + } + if got := selectIPByHardwareAdapters(adapters[2:3], nics[:2], cidr); got != "" { + t.Fatalf("index 2 before that NIC has an IP = %q, want empty", got) + } +} + +func TestSelectIPByHardwareAdapters_skipsIPv6UntilMatch(t *testing.T) { + _, cidr, err := net.ParseCIDR("192.168.86.0/24") + if err != nil { + t.Fatal(err) + } + adapters := []ethernetAdapter{{name: "ethernet-0", mac: "00:50:56:81:9a:a5"}} + nics := []types.GuestNicInfo{ + { + MacAddress: "00:50:56:81:9a:a5", + IpConfig: &types.NetIpConfigInfo{ + IpAddress: []types.NetIpConfigInfoIpAddress{ + {IpAddress: "fe80::1"}, + {IpAddress: "192.168.86.201"}, + }, + }, + }, + } + if got := selectIPByHardwareAdapters(adapters, nics, cidr); got != "192.168.86.201" { + t.Fatalf("got %q, want 192.168.86.201", got) + } +} + +func TestIpMatchesFilter(t *testing.T) { + _, cidr, _ := net.ParseCIDR("192.168.0.0/16") + + if !ipMatchesFilter("192.168.1.1", cidr) { + t.Fatal("expected match in cidr") + } + if ipMatchesFilter("10.0.0.1", cidr) { + t.Fatal("expected no match outside cidr") + } + if !ipMatchesFilter("10.0.0.1", nil) { + t.Fatal("expected ipv4 match with nil ipNet") + } + if ipMatchesFilter("2001:db8::1", nil) { + t.Fatal("expected ipv6 skip with nil ipNet") + } +} + +func TestIpNetWantsIPv4(t *testing.T) { + if !ipNetWantsIPv4(nil) { + t.Fatal("expected nil ipNet to wait for ipv4") + } + + _, v4, err := net.ParseCIDR("0.0.0.0/0") + if err != nil { + t.Fatal(err) + } + if !ipNetWantsIPv4(v4) { + t.Fatal("expected default ipv4 cidr to wait for ipv4") + } + + _, v6, err := net.ParseCIDR("::/0") + if err != nil { + t.Fatal(err) + } + if ipNetWantsIPv4(v6) { + t.Fatal("expected ipv6 cidr to wait for ipv6") + } +} diff --git a/builder/vsphere/driver/vm_mock.go b/builder/vsphere/driver/vm_mock.go index 6426fead..91ea455a 100644 --- a/builder/vsphere/driver/vm_mock.go +++ b/builder/vsphere/driver/vm_mock.go @@ -181,7 +181,7 @@ func (vm *VirtualMachineMock) PowerOn() error { return nil } -func (vm *VirtualMachineMock) WaitForIP(ctx context.Context, ipNet *net.IPNet) (string, error) { +func (vm *VirtualMachineMock) WaitForIP(ctx context.Context, ipNet *net.IPNet, adapterIndex *int) (string, error) { return "", nil } diff --git a/builder/vsphere/iso/config.go b/builder/vsphere/iso/config.go index ed9c9a7c..92be6306 100644 --- a/builder/vsphere/iso/config.go +++ b/builder/vsphere/iso/config.go @@ -8,6 +8,8 @@ package iso import ( + "fmt" + packerCommon "github.com/hashicorp/packer-plugin-sdk/common" "github.com/hashicorp/packer-plugin-sdk/communicator" "github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps" @@ -125,6 +127,9 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { errs = packersdk.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...) errs = packersdk.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...) errs = packersdk.MultiErrorAppend(errs, c.WaitIpConfig.Prepare()...) + if c.WaitAdapterIndex != nil && len(c.NICs) > 0 && *c.WaitAdapterIndex >= len(c.NICs) { + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("ip_wait_adapter_index %d is out of range for %d network_adapters", *c.WaitAdapterIndex, len(c.NICs))) + } errs = packersdk.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...) errs = packersdk.MultiErrorAppend(errs, c.VAppConfig.PrepareSSH(c.Comm)...) diff --git a/builder/vsphere/iso/config.hcl2spec.go b/builder/vsphere/iso/config.hcl2spec.go index d7385e0c..ace54b4c 100644 --- a/builder/vsphere/iso/config.hcl2spec.go +++ b/builder/vsphere/iso/config.hcl2spec.go @@ -99,6 +99,7 @@ type FlatConfig struct { WaitTimeout *string `mapstructure:"ip_wait_timeout" cty:"ip_wait_timeout" hcl:"ip_wait_timeout"` SettleTimeout *string `mapstructure:"ip_settle_timeout" cty:"ip_settle_timeout" hcl:"ip_settle_timeout"` WaitAddress *string `mapstructure:"ip_wait_address" cty:"ip_wait_address" hcl:"ip_wait_address"` + WaitAdapterIndex *int `mapstructure:"ip_wait_adapter_index" cty:"ip_wait_adapter_index" hcl:"ip_wait_adapter_index"` DisableIpWait *bool `mapstructure:"disable_ip_wait" cty:"disable_ip_wait" hcl:"disable_ip_wait"` Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"` PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"` @@ -267,6 +268,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "ip_wait_timeout": &hcldec.AttrSpec{Name: "ip_wait_timeout", Type: cty.String, Required: false}, "ip_settle_timeout": &hcldec.AttrSpec{Name: "ip_settle_timeout", Type: cty.String, Required: false}, "ip_wait_address": &hcldec.AttrSpec{Name: "ip_wait_address", Type: cty.String, Required: false}, + "ip_wait_adapter_index": &hcldec.AttrSpec{Name: "ip_wait_adapter_index", Type: cty.Number, Required: false}, "disable_ip_wait": &hcldec.AttrSpec{Name: "disable_ip_wait", Type: cty.Bool, Required: false}, "communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false}, "pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false}, diff --git a/docs-partials/builder/vsphere/common/DiskConfig-not-required.mdx b/docs-partials/builder/vsphere/common/DiskConfig-not-required.mdx index 52147f68..3cbd3863 100644 --- a/docs-partials/builder/vsphere/common/DiskConfig-not-required.mdx +++ b/docs-partials/builder/vsphere/common/DiskConfig-not-required.mdx @@ -6,8 +6,8 @@ - `disk_eagerly_scrub` (bool) - Enable eager scrubbing for the disk. Defaults to `false`. -- `disk_controller_index` (int) - The assigned disk controller for the disk. - Defaults to the first controller, `(0)`. +- `disk_controller_index` (int) - The assigned disk controller for the disk at the zero-based index + (0, 1, 2, ...). Defaults to the first controller, `(0)`. Mutually exclusive with `disk_controller_unit`. - `disk_controller_unit` (string) - Explicit controller address for the disk when cloning from a `template` diff --git a/docs-partials/builder/vsphere/common/WaitIpConfig-not-required.mdx b/docs-partials/builder/vsphere/common/WaitIpConfig-not-required.mdx index 22e59d28..5bf00e62 100644 --- a/docs-partials/builder/vsphere/common/WaitIpConfig-not-required.mdx +++ b/docs-partials/builder/vsphere/common/WaitIpConfig-not-required.mdx @@ -1,33 +1,37 @@ -- `ip_wait_timeout` (duration string | ex: "1h5m2s") - Amount of time to wait for VM's IP, similar to 'ssh_timeout'. - Defaults to `30m` (30 minutes). Refer to the Golang +- `ip_wait_timeout` (duration string | ex: "1h5m2s") - The amount of time to wait for virtual machine's IP, similar to + `ssh_timeout`. Defaults to `30m` (30 minutes). Refer to the Golang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) - documentation for full details. + documentation for more information. -- `ip_settle_timeout` (duration string | ex: "1h5m2s") - Amount of time to wait for VM's IP to settle down, sometimes VM may - report incorrect IP initially, then it is recommended to set that - parameter to apx. 2 minutes. Examples `45s` and `10m`. - Defaults to `5s` (5 seconds). Refer to the Golang - [ParseDuration](https://golang.org/pkg/time/#ParseDuration) - documentation for full details. +- `ip_settle_timeout` (duration string | ex: "1h5m2s") - The amount of time to wait for virtual machine's IP to settle down. For + example, `45s` and `10m`. Defaults to `5s` (5 seconds). Refer to the + Golang [ParseDuration](https://golang.org/pkg/time/#ParseDuration) + documentation for more information. -- `ip_wait_address` (\*string) - Set this to a CIDR address to cause the service to wait for an address that is contained in - this network range. Defaults to `0.0.0.0/0` for any IPv4 address. +- `ip_wait_address` (\*string) - The IP address range in CIDR notation to wait for. Defaults to + `0.0.0.0/0` for any IPv4 address. - -> **Note:** This only filters which guest-reported IP is accepted; it does not disable IP wait. Use `disable_ip_wait` to skip - waiting for a guest-reported IP entirely. When `disable_ip_wait` is true, this setting still applies to HTTP IP discovery. + -> **Note:** This only filters which guest-reported IP is accepted; it + does not disable IP wait. Use `disable_ip_wait` to skip waiting for a + guest-reported IP entirely. When `disable_ip_wait` is `true`, this + setting still applies to HTTP IP discovery. Examples include: * empty string ("") - remove all filters - * `0:0:0:0:0:0:0:0/0` - allow only ipv6 addresses - * `192.168.1.0/24` - only allow ipv4 addresses from 192.168.1.1 to 192.168.1.254 + * `0:0:0:0:0:0:0:0/0` - allow only IPv6 addresses + * `192.168.1.0/24` - only allow IPv4 addresses from 192.168.1.1 to 192.168.1.254 + +- `ip_wait_adapter_index` (\*int) - When set, wait for an IP on a specific network adapter at this zero-based + index (0, 1, 2, ...). -- `disable_ip_wait` (bool) - When true, skip waiting for a guest-reported IP from vCenter. The default wait relies on - VMware Tools or open-vm-tools guest information. Use when they cannot be installed during - guest operating system install. Defaults to `false`. +- `disable_ip_wait` (bool) - When `true`, skip waiting for a guest-reported IP from vCenter. The + default wait relies on VMware Tools or open-vm-tools guest information. + Use when they cannot be installed during guest operating system install. + Defaults to `false`. - -> **Note:** You must set `ssh_host` or `winrm_host`; reachability timing uses `ssh_timeout` - or `winrm_timeout`, not `ip_wait_timeout`. + -> **Note:** You must set `ssh_host` or `winrm_host`; reachability timing + uses `ssh_timeout` or `winrm_timeout`, not `ip_wait_timeout`.