Skip to content

Commit

Permalink
feat: add support for http_interface
Browse files Browse the repository at this point in the history
Adds support for `http_interface` and `http_bind_address`.

Signed-off-by: Ryan Johnson <[email protected]>
  • Loading branch information
tenthirtyam committed Aug 9, 2024
1 parent 8e5830a commit bca604f
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 37 deletions.
29 changes: 21 additions & 8 deletions .web-docs/components/builder/vsphere-clone/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -773,14 +773,6 @@ For more examples of various boot commands, see the sample projects from our
<!-- End of code generated from the comments of the BootConfig struct in bootcommand/config.go; -->


<!-- Code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; DO NOT EDIT MANUALLY -->

- `http_ip` (string) - The IP address to use for the HTTP server started to serve the `http_directory`.
If unset, Packer will automatically discover and assign an IP.

<!-- End of code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; -->


### HTTP Directory Configuration

<!-- Code generated from the comments of the HTTPConfig struct in multistep/commonsteps/http_config.go; DO NOT EDIT MANUALLY -->
Expand Down Expand Up @@ -839,6 +831,27 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg
<!-- End of code generated from the comments of the HTTPConfig struct in multistep/commonsteps/http_config.go; -->


- `http_interface` (string) - The network interface (for example, `en0`, `ens192`, etc.) that the
HTTP server will use to serve the `http_directory`. The plugin will identify the IP address
associated with this network interface and bind to it.

<!-- Code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; DO NOT EDIT MANUALLY -->

- `http_ip` (string) - The IP address to use for the HTTP server to serve the `http_directory`.

<!-- End of code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; -->


~> **Notes:**
- The options `http_bind_address` and `http_interface` are mutually exclusive.
- Both `http_bind_address` and `http_interface` have higher priority than `http_ip`.
- The `http_bind_address` is matched against the IP addresses of the host's network interfaces. If
no match is found, the plugin will terminate.
- Similarly, `http_interface` is compared with the host's network interfaces. If there's no
corresponding network interface, the plugin will also terminate.
- If neither `http_bind_address`, `http_interface`, and `http_ip` are provided, the plugin will
automatically find and use the IP address of the first non-loopback interface for `http_ip`.

### Floppy Configuration

**Optional:**
Expand Down
29 changes: 21 additions & 8 deletions .web-docs/components/builder/vsphere-iso/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,27 @@ wget http://{{ .HTTPIP }}:{{ .HTTPPort }}/foo/bar/preseed.cfg
<!-- End of code generated from the comments of the HTTPConfig struct in multistep/commonsteps/http_config.go; -->


- `http_interface` (string) - The network interface (for example, `en0`, `ens192`, etc.) that the
HTTP server will use to serve the `http_directory`. The plugin will identify the IP address
associated with this network interface and bind to it.

<!-- Code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; DO NOT EDIT MANUALLY -->

- `http_ip` (string) - The IP address to use for the HTTP server to serve the `http_directory`.

<!-- End of code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; -->


~> **Notes:**
- The options `http_bind_address` and `http_interface` are mutually exclusive.
- Both `http_bind_address` and `http_interface` have higher priority than `http_ip`.
- The `http_bind_address` is matched against the IP addresses of the host's network interfaces. If
no match is found, the plugin will terminate.
- Similarly, `http_interface` is compared with the host's network interfaces. If there's no
corresponding network interface, the plugin will also terminate.
- If neither `http_bind_address`, `http_interface`, and `http_ip` are provided, the plugin will
automatically find and use the IP address of the first non-loopback interface for `http_ip`.

### Connection Configuration

**Optional**:
Expand Down Expand Up @@ -1087,14 +1108,6 @@ JSON Example:
<!-- End of code generated from the comments of the BootConfig struct in bootcommand/config.go; -->


<!-- Code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; DO NOT EDIT MANUALLY -->

- `http_ip` (string) - The IP address to use for the HTTP server started to serve the `http_directory`.
If unset, Packer will automatically discover and assign an IP.

<!-- End of code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; -->


### Wait Configuration

**Optional**:
Expand Down
24 changes: 22 additions & 2 deletions builder/vsphere/clone/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,30 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
Host: b.config.Host,
SetHostForDatastoreUploads: b.config.SetHostForDatastoreUploads,
},
&common.StepHTTPIPDiscover{
)

// Set the address for the HTTP server based on the configuration
// provided by the user.
if addrs := b.config.HTTPConfig.HTTPAddress; addrs != "" && addrs != common.DefaultHttpBindAddress {
// Use the specified HTTPAddress, if valid.
err := common.ValidateHTTPAddress(addrs)
if err != nil {
ui.Errorf("error validating IP address for HTTP server: %s", err)
return nil, err
}
state.Put("http_bind_address", addrs)
} else if intf := b.config.HTTPConfig.HTTPInterface; intf != "" {
// Use the specified HTTPInterface, if valid.
state.Put("http_interface", intf)
} else {
// Use IP discovery if neither is specified.
steps = append(steps, &common.StepHTTPIPDiscover{
HTTPIP: b.config.BootConfig.HTTPIP,
Network: b.config.WaitIpConfig.GetIPNet(),
},
})
}

steps = append(steps,
commonsteps.HTTPServerFromHTTPConfig(&b.config.HTTPConfig),
&common.StepSshKeyPair{
Debug: b.config.PackerDebug,
Expand Down
63 changes: 63 additions & 0 deletions builder/vsphere/common/http_address.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package common

import (
"fmt"
"net"
)

// DefaultHttpBindAddress defines the default IP address for the HTTP server.
const DefaultHttpBindAddress = "0.0.0.0"

// ValidateHTTPAddress validates if the provided HTTP address is valid and
// assigned to an interface.
func ValidateHTTPAddress(httpAddress string) error {
if httpAddress == "" {
return fmt.Errorf("address cannot be empty")
}
if httpAddress == DefaultHttpBindAddress {
return fmt.Errorf("default bind address %s is not allowed", DefaultHttpBindAddress)
}
if net.ParseIP(httpAddress) == nil {
return fmt.Errorf("invalid IP address format: %s", httpAddress)
}
if !IsIPInInterfaces(httpAddress) {
return fmt.Errorf("%s is not assigned to an interface", httpAddress)
}
return nil
}

// IsIPInInterfaces checks if the provided IP address is assigned to any
// interface in the system.
func IsIPInInterfaces(ipStr string) bool {
interfaces, err := net.Interfaces()
if err != nil {
return false
}

for _, i := range interfaces {
addrs, err := i.Addrs()
if err != nil {
continue
}

for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}

parsedIP := net.ParseIP(ipStr)
if ip.Equal(parsedIP) {
return true
}
}
}

return false
}
99 changes: 88 additions & 11 deletions builder/vsphere/common/step_boot_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@ package common
import (
"context"
"fmt"
"log"
"net"
"time"

"github.com/hashicorp/packer-plugin-sdk/bootcommand"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"github.com/hashicorp/packer-plugin-vsphere/builder/vsphere/driver"
"github.com/pkg/errors"
"golang.org/x/mobile/event/key"
)

type BootConfig struct {
bootcommand.BootConfig `mapstructure:",squash"`
// The IP address to use for the HTTP server started to serve the `http_directory`.
// If unset, Packer will automatically discover and assign an IP.
// The IP address to use for the HTTP server to serve the `http_directory`.
HTTPIP string `mapstructure:"http_ip"`
}

Expand All @@ -34,6 +36,7 @@ func (c *BootConfig) Prepare(ctx *interpolate.Context) []error {
if c.BootWait == 0 {
c.BootWait = 10 * time.Second
}

return c.BootConfig.Prepare(ctx)
}

Expand Down Expand Up @@ -68,17 +71,62 @@ func (s *StepBootCommand) Run(ctx context.Context, state multistep.StateBag) mul
pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn)
}

port := state.Get("http_port").(int)
if port > 0 {
ip := state.Get("http_ip").(string)
s.Ctx.Data = &bootCommandTemplateData{
ip,
port,
s.VMName,
var ip string
var err error
port, ok := state.Get("http_port").(int)
if !ok {
ui.Error("error retrieving 'http_port' from state")
return multistep.ActionHalt
}

keys := []string{"http_bind_address", "http_interface", "http_ip"}
for _, key := range keys {
value, ok := state.Get(key).(string)
if !ok || value == "" {
continue
}
ui.Sayf("HTTP server is working at http://%v:%v/", ip, port)

switch key {
case "http_bind_address":
ip = value
log.Printf("Using IP address %s from %s.", ip, key)
case "http_interface":
ip, err = hostIP(value)
if err != nil {
err := fmt.Errorf("error using interface %s: %s", value, err)
state.Put("error", err)
ui.Errorf("%s", err)
return multistep.ActionHalt
}
log.Printf("Using IP address %s from %s %s.", ip, key, value)
case "http_ip":
if err := ValidateHTTPAddress(value); err != nil {
err := fmt.Errorf("error using IP address %s: %s", value, err)
state.Put("error", err)
ui.Errorf("%s", err)
return multistep.ActionHalt
}
ip = value
log.Printf("Using IP address %s from %s.", ip, key)
}
}

// Check if IP address was determined.
if ip == "" {
err := fmt.Errorf("error determining IP address")
state.Put("error", err)
ui.Errorf("%s", err)
return multistep.ActionHalt
}

s.Ctx.Data = &bootCommandTemplateData{
ip,
port,
s.VMName,
}

ui.Sayf("Serving HTTP requests at http://%v:%v/.", ip, port)

var keyAlt, keyCtrl, keyShift bool
sendCodes := func(code key.Code, down bool) error {
switch code {
Expand All @@ -104,7 +152,7 @@ func (s *StepBootCommand) Run(ctx context.Context, state multistep.StateBag) mul
if err != nil {
// retry once if error
ui.Errorf("error typing a boot command (code, down) `%d, %t`: %v", code, down, err)
ui.Say("Trying key input again...")
ui.Say("Trying boot command again...")
time.Sleep(s.Config.BootGroupInterval)
_, err = vm.TypeOnKeyboard(driver.KeyInput{
Scancode: code,
Expand Down Expand Up @@ -153,3 +201,32 @@ func (s *StepBootCommand) Run(ctx context.Context, state multistep.StateBag) mul
}

func (s *StepBootCommand) Cleanup(_ multistep.StateBag) {}

func hostIP(ifname string) (string, error) {
var addrs []net.Addr
var err error

if ifname != "" {
iface, err := net.InterfaceByName(ifname)
if err != nil {
return "", err
}
addrs, err = iface.Addrs()
if err != nil {
return "", err
}
} else {
addrs, err = net.InterfaceAddrs()
if err != nil {
return "", err
}
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String(), nil
}
}
}
return "", errors.New("error returning host ip address")
}
1 change: 1 addition & 0 deletions builder/vsphere/common/step_http_ip_discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func (s *StepHTTPIPDiscover) Run(ctx context.Context, state multistep.StateBag)
state.Put("error", err)
return multistep.ActionHalt
}

state.Put("http_ip", ip)

return multistep.ActionContinue
Expand Down
25 changes: 23 additions & 2 deletions builder/vsphere/iso/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,31 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
Host: b.config.Host,
SetHostForDatastoreUploads: b.config.SetHostForDatastoreUploads,
},
&common.StepHTTPIPDiscover{
)

// Set the address for the HTTP server based on the configuration
// provided by the user.
if addrs := b.config.HTTPConfig.HTTPAddress; addrs != "" && addrs != common.DefaultHttpBindAddress {
// Validate and use the specified HTTPAddress.
err := common.ValidateHTTPAddress(addrs)
if err != nil {
ui.Errorf("error validating IP address for HTTP server: %s", err)
return nil, err
}
state.Put("http_bind_address", addrs)
} else if intf := b.config.HTTPConfig.HTTPInterface; intf != "" {
// Use the specified HTTPInterface.
state.Put("http_interface", intf)
} else {
// Use IP discovery if neither HTTPAddress nor HTTPInterface
// is specified.
steps = append(steps, &common.StepHTTPIPDiscover{
HTTPIP: b.config.BootConfig.HTTPIP,
Network: b.config.WaitIpConfig.GetIPNet(),
},
})
}

steps = append(steps,
commonsteps.HTTPServerFromHTTPConfig(&b.config.HTTPConfig),
&common.StepRun{
Config: &b.config.RunConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<!-- Code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; DO NOT EDIT MANUALLY -->

- `http_ip` (string) - The IP address to use for the HTTP server started to serve the `http_directory`.
If unset, Packer will automatically discover and assign an IP.
- `http_ip` (string) - The IP address to use for the HTTP server to serve the `http_directory`.

<!-- End of code generated from the comments of the BootConfig struct in builder/vsphere/common/step_boot_command.go; -->
Loading

0 comments on commit bca604f

Please sign in to comment.