diff --git a/builder/vmware/common/driver.go b/builder/vmware/common/driver.go index 82d1ae1f..fe8efaaf 100644 --- a/builder/vmware/common/driver.go +++ b/builder/vmware/common/driver.go @@ -323,6 +323,9 @@ type Driver interface { // HostIP retrieves the host IP address for the virtual machine based on the state. HostIP(multistep.StateBag) (string, error) + // GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools. + GetGuestIPAddress(string) (string, error) + // Export exports a virtual machine using the provided arguments. Export([]string) error @@ -484,6 +487,10 @@ type VmwareDriver struct { // This method returns an object with the NetworkNameMapper interface // that maps network to device and vice versa. NetworkMapper func() (NetworkNameMapper, error) + + // IPFinder returns the IP address for a given device. If nil, getHostIPForBridgedNetwork is used. + // This allows for mocking in tests. + IPFinder func(device string) (string, error) } // GuestAddress retrieves the MAC address of a guest virtual machine from the .vmx configuration. @@ -586,7 +593,6 @@ func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) { var lastError error for _, device := range devices { - // parse dhcpd configuration pathDhcpConfig := d.DhcpConfPath(device) if _, err := os.Stat(pathDhcpConfig); err != nil { return "", fmt.Errorf("unable to find vmnetdhcp conf file: %s", pathDhcpConfig) @@ -613,7 +619,7 @@ func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) { // we didn't find it, so search through our interfaces for the device name interfaceList, err := net.Interfaces() - if err == nil { + if err != nil { return "", err } @@ -667,7 +673,31 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) { var lastError error for _, device := range devices { - // parse dhcpd configuration + // Check if this is a bridged network device. + networkName, err := netmap.DeviceIntoName(device) + isBridged := err == nil && strings.EqualFold(networkName, "bridged") + + if isBridged { + // Bridged networks connect to the physical network. + // Find the host's IP address on the physical network interface. + log.Printf("[INFO] Detected bridged network for device %s, finding host IP on physical network", device) + + var address string + var err error + if d.IPFinder != nil { + address, err = d.IPFinder(device) + } else { + // Get the first non-loopback interface IP address. + address, err = getHostIPForBridgedNetwork() + } + if err != nil { + lastError = err + continue + } + return address, nil + } + + // For non-bridged networks, use the DHCP leases path. pathDhcpConfig := d.DhcpConfPath(device) if _, err := os.Stat(pathDhcpConfig); err != nil { return "", fmt.Errorf("unable to find vmnetdhcp conf file: %s", pathDhcpConfig) @@ -678,7 +708,6 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) { continue } - // find the entry configured in the dhcpd interfaceConfig, err := config.HostByName(device) if err != nil { lastError = err @@ -696,6 +725,38 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) { return "", fmt.Errorf("unable to find host IP from devices %v, last error: %s", devices, lastError) } +// getHostIPForBridgedNetwork returns the host's IP address on the physical +// network for use with bridged networking. +func getHostIPForBridgedNetwork() (string, error) { + interfaces, err := net.Interfaces() + if err != nil { + return "", fmt.Errorf("unable to enumerate network interfaces: %s", err) + } + + for _, iface := range interfaces { + // Skip loopback and down interfaces. + if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { + continue + } + + addrs, err := iface.Addrs() + if err != nil { + continue + } + + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ipv4 := ipnet.IP.To4(); ipv4 != nil { + log.Printf("[INFO] Found host IP for bridged network: %s on interface %s", ipv4.String(), iface.Name) + return ipv4.String(), nil + } + } + } + } + + return "", fmt.Errorf("unable to find a non-loopback IPv4 address on any interface") +} + // GetDhcpLeasesPaths returns a copy of the DHCP leases paths. func GetDhcpLeasesPaths() []string { return append([]string(nil), dhcpLeasesPaths...) diff --git a/builder/vmware/common/driver_fusion.go b/builder/vmware/common/driver_fusion.go index 0202080a..33b53fda 100644 --- a/builder/vmware/common/driver_fusion.go +++ b/builder/vmware/common/driver_fusion.go @@ -8,6 +8,7 @@ import ( "bytes" "fmt" "log" + "net" "os" "os/exec" "path/filepath" @@ -324,6 +325,36 @@ func (d *FusionDriver) GetVmwareDriver() VmwareDriver { return d.VmwareDriver } +// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools. +func (d *FusionDriver) GetGuestIPAddress(vmxPath string) (string, error) { + cleanVmx := filepath.Clean(vmxPath) + absVmxPath, err := filepath.Abs(cleanVmx) + if err != nil { + return "", fmt.Errorf("failed to get absolute path for .vmx: %s", err) + } + + cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "getGuestIPAddress", absVmxPath, "-wait") //nolint:gosec + output, err := cmd.Output() + if err != nil { + // VMware Tools might not be running yet. + return "", fmt.Errorf("failed to retrieve IP address using VMware Tools: %s", err) + } + + // Parse the IP address from output. + ipAddr := strings.TrimSpace(string(output)) + if ipAddr == "" { + return "", fmt.Errorf("returned an empty IP address") + } + + // Validate the IP address. + if net.ParseIP(ipAddr) == nil { + return "", fmt.Errorf("returned an invalid IP address: %s", ipAddr) + } + + log.Printf("[INFO] Discovered guest IP address using VMware Tools: %s", ipAddr) + return ipAddr, nil +} + func (d *FusionDriver) getFusionVersion() (*version.Version, error) { var stderr bytes.Buffer diff --git a/builder/vmware/common/driver_mock.go b/builder/vmware/common/driver_mock.go index 59001d83..528f992b 100644 --- a/builder/vmware/common/driver_mock.go +++ b/builder/vmware/common/driver_mock.go @@ -304,3 +304,7 @@ func (d *DriverMock) GetVmwareDriver() VmwareDriver { func (d *DriverMock) VerifyOvfTool(_ bool, _ bool) error { return nil } + +func (d *DriverMock) GetGuestIPAddress(vmxPath string) (string, error) { + return "192.168.1.100", nil +} diff --git a/builder/vmware/common/driver_parser.go b/builder/vmware/common/driver_parser.go index 02e5cb12..9eee98fc 100644 --- a/builder/vmware/common/driver_parser.go +++ b/builder/vmware/common/driver_parser.go @@ -1103,7 +1103,11 @@ func (e *ConfigDeclaration) Hardware() (net.HardwareAddr, error) { } } - if len(result) > 0 { + if len(result) == 0 { + return nil, fmt.Errorf("no hardware address found") + } + + if len(result) > 1 { return nil, fmt.Errorf("more than one hardware address returned : %v", result) } diff --git a/builder/vmware/common/driver_workstation.go b/builder/vmware/common/driver_workstation.go index ce215bb7..14231aa1 100644 --- a/builder/vmware/common/driver_workstation.go +++ b/builder/vmware/common/driver_workstation.go @@ -9,6 +9,7 @@ import ( "fmt" "html/template" "log" + "net" "os" "os/exec" "path/filepath" @@ -78,6 +79,30 @@ func (d *WorkstationDriver) GetVmwareDriver() VmwareDriver { return d.VmwareDriver } +// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools. +func (d *WorkstationDriver) GetGuestIPAddress(vmxPath string) (string, error) { + cmd := exec.Command(d.VmrunPath, "-T", "ws", "getGuestIPAddress", vmxPath, "-wait") + output, err := cmd.Output() + if err != nil { + // VMware Tools might not be running yet. + return "", fmt.Errorf("failed to retrieve IP address using VMware Tools: %s", err) + } + + // Parse the IP address from output. + ipAddr := strings.TrimSpace(string(output)) + if ipAddr == "" { + return "", fmt.Errorf("returned an empty IP address") + } + + // Validate the IP address. + if net.ParseIP(ipAddr) == nil { + return "", fmt.Errorf("returned an invalid IP address: %s", ipAddr) + } + + log.Printf("[INFO] Discovered guest IP address using VMware Tools: %s", ipAddr) + return ipAddr, nil +} + // Clone creates a copy of the source virtual machine at the destination path. func (d *WorkstationDriver) Clone(dst, src string, linked bool, snapshot string) error { diff --git a/builder/vmware/common/ssh.go b/builder/vmware/common/ssh.go index eafa7930..4986e492 100644 --- a/builder/vmware/common/ssh.go +++ b/builder/vmware/common/ssh.go @@ -9,13 +9,15 @@ import ( "fmt" "log" "net" + "strings" "github.com/hashicorp/packer-plugin-sdk/multistep" "github.com/hashicorp/packer-plugin-sdk/sdk-internals/communicator/ssh" "golang.org/x/net/proxy" ) -// CommHost returns a function that determines the IP address of the guest that is ready to accept connections. +// CommHost returns a function that determines the IP address of the guest that +// is ready to accept connections. func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) { return func(state multistep.StateBag) (string, error) { driver := state.Get("driver").(Driver) @@ -28,10 +30,38 @@ func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) { port := comm.Port() - // Get the list of potential addresses that the guest might use. - hosts, err := driver.PotentialGuestIP(state) - if err != nil { - return "", fmt.Errorf("failed to lookup IP address: %s", err) + // Check if this is a bridged network (case-insensitive). + network := state.Get("vmnetwork").(string) + isBridged := strings.EqualFold(network, "bridged") + + var hosts []string + var err error + + if isBridged { + // For bridged networks, wait for VMware Tools to provide the IP address. + if state.Get("vmtools_ip_attempt") == nil { + log.Printf("[INFO] Waiting for IP address from VMware Tools...") + state.Put("vmtools_ip_attempt", true) + } + + vmxPath := state.Get("vmx_path").(string) + if addr, vmrunErr := driver.GetGuestIPAddress(vmxPath); vmrunErr == nil && addr != "" { + hosts = []string{addr} + } else { + return "", fmt.Errorf("waiting for VMware Tools to start: %s", vmrunErr) + } + } else { + // For NAT/host-only networks, use DHCP leases as the primary method. + hosts, err = driver.PotentialGuestIP(state) + if err != nil { + // Fallback: Check to see if VMware Tools can provide the IP address. + vmxPath := state.Get("vmx_path").(string) + if addr, vmrunErr := driver.GetGuestIPAddress(vmxPath); vmrunErr == nil && addr != "" { + hosts = []string{addr} + } else { + return "", fmt.Errorf("failed to lookup IP address: %s", err) + } + } } if len(hosts) == 0 { @@ -54,15 +84,15 @@ func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) { var connFunc func() (net.Conn, error) for _, host := range hosts { if pAddr != "" { - // Connect through a bastion host. + // Connect using a bastion host. connFunc = ssh.ProxyConnectFunc(pAddr, pAuth, "tcp", fmt.Sprintf("%s:%d", host, port)) } else { - // Connect directly to the host. + // Connect directly. connFunc = ssh.ConnectFunc("tcp", fmt.Sprintf("%s:%d", host, port)) } conn, err := connFunc() - // If we can connect, then we can use this IP address. + // If the connection is successful, use this IP address. if err == nil { err := conn.Close() if err != nil {