Skip to content

Commit e72ff9c

Browse files
committed
refactor: add support for bridged networking
Refactors to add support bridged networking. - Adds the ability to retrieve the IP address of the guest operating system from VMware Tools. - Detects the IP address of the host to use when using bridged networking. - Uses the IP address of the guest operating system received from VMware Tools when using bridged networking. - Uses the IP address of the guest operating system received from VMware Tools as a fallback for NAT and host-only networking. Note: The method for NAT and host-only networking may be subsequently refactored to only use the VMware Tools method, but this commit limits the scope to only adding support for bridged networking. Signed-off-by: Ryan Johnson <ryan@tenthirtyam.org>
1 parent 79d9ece commit e72ff9c

6 files changed

Lines changed: 168 additions & 13 deletions

File tree

builder/vmware/common/driver.go

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ type Driver interface {
323323
// HostIP retrieves the host IP address for the virtual machine based on the state.
324324
HostIP(multistep.StateBag) (string, error)
325325

326+
// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools.
327+
GetGuestIPAddress(string) (string, error)
328+
326329
// Export exports a virtual machine using the provided arguments.
327330
Export([]string) error
328331

@@ -484,6 +487,10 @@ type VmwareDriver struct {
484487
// This method returns an object with the NetworkNameMapper interface
485488
// that maps network to device and vice versa.
486489
NetworkMapper func() (NetworkNameMapper, error)
490+
491+
// IPFinder returns the IP address for a given device. If nil, getHostIPForBridgedNetwork is used.
492+
// This allows for mocking in tests.
493+
IPFinder func(device string) (string, error)
487494
}
488495

489496
// 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) {
586593

587594
var lastError error
588595
for _, device := range devices {
589-
// parse dhcpd configuration
590596
pathDhcpConfig := d.DhcpConfPath(device)
591597
if _, err := os.Stat(pathDhcpConfig); err != nil {
592598
return "", fmt.Errorf("unable to find vmnetdhcp conf file: %s", pathDhcpConfig)
@@ -613,7 +619,7 @@ func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) {
613619

614620
// we didn't find it, so search through our interfaces for the device name
615621
interfaceList, err := net.Interfaces()
616-
if err == nil {
622+
if err != nil {
617623
return "", err
618624
}
619625

@@ -667,7 +673,31 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) {
667673

668674
var lastError error
669675
for _, device := range devices {
670-
// parse dhcpd configuration
676+
// Check if this is a bridged network device.
677+
networkName, err := netmap.DeviceIntoName(device)
678+
isBridged := err == nil && strings.EqualFold(networkName, "bridged")
679+
680+
if isBridged {
681+
// Bridged networks connect to the physical network.
682+
// Find the host's IP address on the physical network interface.
683+
log.Printf("[INFO] Detected bridged network for device %s, finding host IP on physical network", device)
684+
685+
var address string
686+
var err error
687+
if d.IPFinder != nil {
688+
address, err = d.IPFinder(device)
689+
} else {
690+
// Get the first non-loopback interface IP address.
691+
address, err = getHostIPForBridgedNetwork()
692+
}
693+
if err != nil {
694+
lastError = err
695+
continue
696+
}
697+
return address, nil
698+
}
699+
700+
// For non-bridged networks, use the DHCP leases path.
671701
pathDhcpConfig := d.DhcpConfPath(device)
672702
if _, err := os.Stat(pathDhcpConfig); err != nil {
673703
return "", fmt.Errorf("unable to find vmnetdhcp conf file: %s", pathDhcpConfig)
@@ -678,7 +708,6 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) {
678708
continue
679709
}
680710

681-
// find the entry configured in the dhcpd
682711
interfaceConfig, err := config.HostByName(device)
683712
if err != nil {
684713
lastError = err
@@ -696,6 +725,38 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) {
696725
return "", fmt.Errorf("unable to find host IP from devices %v, last error: %s", devices, lastError)
697726
}
698727

728+
// getHostIPForBridgedNetwork returns the host's IP address on the physical
729+
// network for use with bridged networking.
730+
func getHostIPForBridgedNetwork() (string, error) {
731+
interfaces, err := net.Interfaces()
732+
if err != nil {
733+
return "", fmt.Errorf("unable to enumerate network interfaces: %s", err)
734+
}
735+
736+
for _, iface := range interfaces {
737+
// Skip loopback and down interfaces.
738+
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
739+
continue
740+
}
741+
742+
addrs, err := iface.Addrs()
743+
if err != nil {
744+
continue
745+
}
746+
747+
for _, addr := range addrs {
748+
if ipnet, ok := addr.(*net.IPNet); ok {
749+
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
750+
log.Printf("[INFO] Found host IP for bridged network: %s on interface %s", ipv4.String(), iface.Name)
751+
return ipv4.String(), nil
752+
}
753+
}
754+
}
755+
}
756+
757+
return "", fmt.Errorf("unable to find a non-loopback IPv4 address on any interface")
758+
}
759+
699760
// GetDhcpLeasesPaths returns a copy of the DHCP leases paths.
700761
func GetDhcpLeasesPaths() []string {
701762
return append([]string(nil), dhcpLeasesPaths...)

builder/vmware/common/driver_fusion.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"fmt"
1010
"log"
11+
"net"
1112
"os"
1213
"os/exec"
1314
"path/filepath"
@@ -324,6 +325,36 @@ func (d *FusionDriver) GetVmwareDriver() VmwareDriver {
324325
return d.VmwareDriver
325326
}
326327

328+
// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools.
329+
func (d *FusionDriver) GetGuestIPAddress(vmxPath string) (string, error) {
330+
cleanVmx := filepath.Clean(vmxPath)
331+
absVmxPath, err := filepath.Abs(cleanVmx)
332+
if err != nil {
333+
return "", fmt.Errorf("failed to get absolute path for .vmx: %s", err)
334+
}
335+
336+
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "getGuestIPAddress", absVmxPath, "-wait") //nolint:gosec
337+
output, err := cmd.Output()
338+
if err != nil {
339+
// VMware Tools might not be running yet.
340+
return "", fmt.Errorf("failed to retrieve IP address using VMware Tools: %s", err)
341+
}
342+
343+
// Parse the IP address from output.
344+
ipAddr := strings.TrimSpace(string(output))
345+
if ipAddr == "" {
346+
return "", fmt.Errorf("returned an empty IP address")
347+
}
348+
349+
// Validate the IP address.
350+
if net.ParseIP(ipAddr) == nil {
351+
return "", fmt.Errorf("returned an invalid IP address: %s", ipAddr)
352+
}
353+
354+
log.Printf("[INFO] Discovered guest IP address using VMware Tools: %s", ipAddr)
355+
return ipAddr, nil
356+
}
357+
327358
func (d *FusionDriver) getFusionVersion() (*version.Version, error) {
328359
var stderr bytes.Buffer
329360

builder/vmware/common/driver_mock.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,7 @@ func (d *DriverMock) GetVmwareDriver() VmwareDriver {
304304
func (d *DriverMock) VerifyOvfTool(_ bool, _ bool) error {
305305
return nil
306306
}
307+
308+
func (d *DriverMock) GetGuestIPAddress(vmxPath string) (string, error) {
309+
return "192.168.1.100", nil
310+
}

builder/vmware/common/driver_parser.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1103,7 +1103,11 @@ func (e *ConfigDeclaration) Hardware() (net.HardwareAddr, error) {
11031103
}
11041104
}
11051105

1106-
if len(result) > 0 {
1106+
if len(result) == 0 {
1107+
return nil, fmt.Errorf("no hardware address found")
1108+
}
1109+
1110+
if len(result) > 1 {
11071111
return nil, fmt.Errorf("more than one hardware address returned : %v", result)
11081112
}
11091113

builder/vmware/common/driver_workstation.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"html/template"
1111
"log"
12+
"net"
1213
"os"
1314
"os/exec"
1415
"path/filepath"
@@ -78,6 +79,30 @@ func (d *WorkstationDriver) GetVmwareDriver() VmwareDriver {
7879
return d.VmwareDriver
7980
}
8081

82+
// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools.
83+
func (d *WorkstationDriver) GetGuestIPAddress(vmxPath string) (string, error) {
84+
cmd := exec.Command(d.VmrunPath, "-T", "ws", "getGuestIPAddress", vmxPath, "-wait")
85+
output, err := cmd.Output()
86+
if err != nil {
87+
// VMware Tools might not be running yet.
88+
return "", fmt.Errorf("failed to retrieve IP address using VMware Tools: %s", err)
89+
}
90+
91+
// Parse the IP address from output.
92+
ipAddr := strings.TrimSpace(string(output))
93+
if ipAddr == "" {
94+
return "", fmt.Errorf("returned an empty IP address")
95+
}
96+
97+
// Validate the IP address.
98+
if net.ParseIP(ipAddr) == nil {
99+
return "", fmt.Errorf("returned an invalid IP address: %s", ipAddr)
100+
}
101+
102+
log.Printf("[INFO] Discovered guest IP address using VMware Tools: %s", ipAddr)
103+
return ipAddr, nil
104+
}
105+
81106
// Clone creates a copy of the source virtual machine at the destination path.
82107
func (d *WorkstationDriver) Clone(dst, src string, linked bool, snapshot string) error {
83108

builder/vmware/common/ssh.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import (
99
"fmt"
1010
"log"
1111
"net"
12+
"strings"
1213

1314
"github.com/hashicorp/packer-plugin-sdk/multistep"
1415
"github.com/hashicorp/packer-plugin-sdk/sdk-internals/communicator/ssh"
1516
"golang.org/x/net/proxy"
1617
)
1718

18-
// CommHost returns a function that determines the IP address of the guest that is ready to accept connections.
19+
// CommHost returns a function that determines the IP address of the guest that
20+
// is ready to accept connections.
1921
func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) {
2022
return func(state multistep.StateBag) (string, error) {
2123
driver := state.Get("driver").(Driver)
@@ -28,10 +30,38 @@ func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) {
2830

2931
port := comm.Port()
3032

31-
// Get the list of potential addresses that the guest might use.
32-
hosts, err := driver.PotentialGuestIP(state)
33-
if err != nil {
34-
return "", fmt.Errorf("failed to lookup IP address: %s", err)
33+
// Check if this is a bridged network (case-insensitive).
34+
network := state.Get("vmnetwork").(string)
35+
isBridged := strings.EqualFold(network, "bridged")
36+
37+
var hosts []string
38+
var err error
39+
40+
if isBridged {
41+
// For bridged networks, wait for VMware Tools to provide the IP address.
42+
if state.Get("vmtools_ip_attempt") == nil {
43+
log.Printf("[INFO] Waiting for IP address from VMware Tools...")
44+
state.Put("vmtools_ip_attempt", true)
45+
}
46+
47+
vmxPath := state.Get("vmx_path").(string)
48+
if addr, vmrunErr := driver.GetGuestIPAddress(vmxPath); vmrunErr == nil && addr != "" {
49+
hosts = []string{addr}
50+
} else {
51+
return "", fmt.Errorf("waiting for VMware Tools to start: %s", vmrunErr)
52+
}
53+
} else {
54+
// For NAT/host-only networks, use DHCP leases as the primary method.
55+
hosts, err = driver.PotentialGuestIP(state)
56+
if err != nil {
57+
// Fallback: Check to see if VMware Tools can provide the IP address.
58+
vmxPath := state.Get("vmx_path").(string)
59+
if addr, vmrunErr := driver.GetGuestIPAddress(vmxPath); vmrunErr == nil && addr != "" {
60+
hosts = []string{addr}
61+
} else {
62+
return "", fmt.Errorf("failed to lookup IP address: %s", err)
63+
}
64+
}
3565
}
3666

3767
if len(hosts) == 0 {
@@ -54,15 +84,15 @@ func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) {
5484
var connFunc func() (net.Conn, error)
5585
for _, host := range hosts {
5686
if pAddr != "" {
57-
// Connect through a bastion host.
87+
// Connect using a bastion host.
5888
connFunc = ssh.ProxyConnectFunc(pAddr, pAuth, "tcp", fmt.Sprintf("%s:%d", host, port))
5989
} else {
60-
// Connect directly to the host.
90+
// Connect directly.
6191
connFunc = ssh.ConnectFunc("tcp", fmt.Sprintf("%s:%d", host, port))
6292
}
6393
conn, err := connFunc()
6494

65-
// If we can connect, then we can use this IP address.
95+
// If the connection is successful, use this IP address.
6696
if err == nil {
6797
err := conn.Close()
6898
if err != nil {

0 commit comments

Comments
 (0)