Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions builder/hcloud/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Config struct {
SSHKeysLabels map[string]string `mapstructure:"ssh_keys_labels"`

Networks []int64 `mapstructure:"networks"`
SSHInterface string `mapstructure:"ssh_interface"`
PublicIPv4 string `mapstructure:"public_ipv4"`
PublicIPv4Disabled bool `mapstructure:"public_ipv4_disabled"`
PublicIPv6 string `mapstructure:"public_ipv6"`
Expand Down Expand Up @@ -153,6 +154,14 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile))
}
}
if c.SSHInterface != "" {
switch c.SSHInterface {
case "public_ipv4", "public_ipv6", "private_ipv4":
default:
errs = packersdk.MultiErrorAppend(
errs, errors.New("ssh_interface must be one of public_ipv4, public_ipv6, or private_ipv4"))
}
}

if errs != nil && len(errs.Errors) > 0 {
return nil, errs
Expand Down
2 changes: 2 additions & 0 deletions builder/hcloud/config.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions builder/hcloud/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package hcloud

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestConfigPrepareSSHInterface(t *testing.T) {
testCases := []struct {
name string
sshInterface string
wantErr string
}{
{
name: "public_ipv4",
sshInterface: "public_ipv4",
},
{
name: "public_ipv6",
sshInterface: "public_ipv6",
},
{
name: "private_ipv4",
sshInterface: "private_ipv4",
},
{
name: "invalid",
sshInterface: "public",
wantErr: "ssh_interface must be one of public_ipv4, public_ipv6, or private_ipv4",
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
c := &Config{}
_, err := c.Prepare(map[string]interface{}{
"token": "dummy-token",
"image": "debian-12",
"location": "nbg1",
"server_type": "cpx22",
"ssh_username": "root",
"ssh_interface": testCase.sshInterface,
})

if testCase.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), testCase.wantErr)
return
}
require.NoError(t, err)
})
}
}
51 changes: 44 additions & 7 deletions builder/hcloud/step_create_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
// instance id inside of the provisioners, used in step_provision.
state.Put(StateInstanceID, server.ID)

serverIP := firstAvailableIP(server)
serverIP := selectedServerIP(server, c.SSHInterface)
if serverIP == "" {
return errorHandler(state, ui, "", fmt.Errorf("Could not find available ip"))
}
Expand Down Expand Up @@ -325,18 +325,55 @@ func getPrimaryIP(ctx context.Context, client *hcloud.Client, publicIP string) (
func firstAvailableIP(server *hcloud.Server) string {
switch {
case !server.PublicNet.IPv4.IsUnspecified():
return server.PublicNet.IPv4.IP.String()
return publicIPv4(server)
case !server.PublicNet.IPv6.IsUnspecified():
network, ok := netip.AddrFromSlice(server.PublicNet.IPv6.IP)
if ok {
return network.Next().String()
}
return publicIPv6(server)
case len(server.PrivateNet) > 0:
return server.PrivateNet[0].IP.String()
return privateIPv4(server)
}
return ""
}

func selectedServerIP(server *hcloud.Server, sshInterface string) string {
switch sshInterface {
case "":
return firstAvailableIP(server)
case "public_ipv4":
return publicIPv4(server)
case "public_ipv6":
return publicIPv6(server)
case "private_ipv4":
return privateIPv4(server)
default:
return ""
}
}

func publicIPv4(server *hcloud.Server) string {
if server.PublicNet.IPv4.IsUnspecified() {
return ""
}
return server.PublicNet.IPv4.IP.String()
}

func publicIPv6(server *hcloud.Server) string {
if server.PublicNet.IPv6.IsUnspecified() {
return ""
}
network, ok := netip.AddrFromSlice(server.PublicNet.IPv6.IP)
if !ok {
return ""
}
return network.Next().String()
}

func privateIPv4(server *hcloud.Server) string {
if len(server.PrivateNet) == 0 {
return ""
}
return server.PrivateNet[0].IP.String()
}

func getServerRunningActions(ctx context.Context, client *hcloud.Client, server *hcloud.Server) ([]*hcloud.Action, error) {
actions, err := client.Firewall.Action.All(ctx,
hcloud.ActionListOpts{
Expand Down
85 changes: 72 additions & 13 deletions builder/hcloud/step_create_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ func TestStepCreateServer(t *testing.T) {
},
},
{
Name: "happy with firewall",
Name: "happy with firewall and public ipv6 communicator",
Step: &stepCreateServer{},
SetupConfigFunc: func(c *Config) {
c.Firewalls = []string{"allow-ssh"}
c.SSHInterface = "public_ipv6"
},
SetupStateFunc: func(state multistep.StateBag) {
state.Put(StateSSHKeyID, int64(1))
Expand Down Expand Up @@ -126,7 +127,7 @@ func TestStepCreateServer(t *testing.T) {
},
Status: 201,
JSONRaw: `{
"server": { "id": 8, "name": "dummy-server", "public_net": { "ipv4": { "ip": "127.0.0.1" }, "ipv6": { "ip": "::1" }}},
"server": { "id": 8, "name": "dummy-server", "public_net": { "ipv4": { "ip": "127.0.0.1" }, "ipv6": { "ip": "2a01:4f8:1c19:1403::/64" }}},
"action": { "id": 3, "status": "running" }
}`,
},
Expand Down Expand Up @@ -160,6 +161,10 @@ func TestStepCreateServer(t *testing.T) {
instanceID, ok := state.Get(StateInstanceID).(int64)
assert.True(t, ok)
assert.Equal(t, int64(8), instanceID)

serverIP, ok := state.Get(StateServerIP).(string)
assert.True(t, ok)
assert.Equal(t, "2a01:4f8:1c19:1403::1", serverIP)
},
},
{
Expand Down Expand Up @@ -631,6 +636,15 @@ func TestStepCreateServer(t *testing.T) {
}

func TestFirstAvailableIP(t *testing.T) {
server := &hcloud.Server{
PublicNet: hcloud.ServerPublicNetFromSchema(schema.ServerPublicNet{
IPv4: schema.ServerPublicNetIPv4{ID: 1, IP: "1.2.3.4"},
IPv6: schema.ServerPublicNetIPv6{ID: 2, IP: "2a01:4f8:1c19:1403::/64"},
}),
PrivateNet: []hcloud.ServerPrivateNet{
hcloud.ServerPrivateNetFromSchema(schema.ServerPrivateNet{Network: 3, IP: "10.0.0.1"}),
},
}
testCases := []struct {
name string
server *hcloud.Server
Expand All @@ -642,17 +656,9 @@ func TestFirstAvailableIP(t *testing.T) {
want: "",
},
{
name: "public_ipv4",
server: &hcloud.Server{
PublicNet: hcloud.ServerPublicNetFromSchema(schema.ServerPublicNet{
IPv4: schema.ServerPublicNetIPv4{ID: 1, IP: "1.2.3.4"},
IPv6: schema.ServerPublicNetIPv6{ID: 2, IP: "2a01:4f8:1c19:1403::/64"},
}),
PrivateNet: []hcloud.ServerPrivateNet{
hcloud.ServerPrivateNetFromSchema(schema.ServerPrivateNet{Network: 3, IP: "10.0.0.1"}),
},
},
want: "1.2.3.4",
name: "public_ipv4",
server: server,
want: "1.2.3.4",
},
{
name: "public_ipv6",
Expand Down Expand Up @@ -683,3 +689,56 @@ func TestFirstAvailableIP(t *testing.T) {
})
}
}

func TestSelectedServerIP(t *testing.T) {
server := &hcloud.Server{
PublicNet: hcloud.ServerPublicNetFromSchema(schema.ServerPublicNet{
IPv4: schema.ServerPublicNetIPv4{ID: 1, IP: "1.2.3.4"},
IPv6: schema.ServerPublicNetIPv6{ID: 2, IP: "2a01:4f8:1c19:1403::/64"},
}),
PrivateNet: []hcloud.ServerPrivateNet{
hcloud.ServerPrivateNetFromSchema(schema.ServerPrivateNet{Network: 3, IP: "10.0.0.1"}),
},
}
testCases := []struct {
name string
sshInterface string
want string
}{
{
name: "default",
sshInterface: "",
want: "1.2.3.4",
},
{
name: "public_ipv4",
sshInterface: "public_ipv4",
want: "1.2.3.4",
},
{
name: "public_ipv6",
sshInterface: "public_ipv6",
want: "2a01:4f8:1c19:1403::1",
},
{
name: "private_ipv4",
sshInterface: "private_ipv4",
want: "10.0.0.1",
},
{
name: "missing selected ip",
sshInterface: "private_ipv4",
want: "",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
testServer := server
if testCase.name == "missing selected ip" {
testServer = &hcloud.Server{}
}
result := selectedServerIP(testServer, testCase.sshInterface)
assert.Equal(t, testCase.want, result)
})
}
}
8 changes: 8 additions & 0 deletions docs/builders/hcloud.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ The builder will connect to the server using the first available IP, in the foll
- `private_ipv4`: If the server is attached to private networks, the private IPv4 of the
first private network will be used.

You can override this automatic selection with `ssh_interface`. Valid values
are `public_ipv4`, `public_ipv6`, and `private_ipv4`.

## Configuration Reference

There are many configuration options available for the builder. They are
Expand Down Expand Up @@ -132,6 +135,11 @@ builder.
- `networks` (array of integers) - List of Network IDs which should be
attached to the server private network interface at creation time.

- `ssh_interface` (string) - Selects which server IP address the communicator
should use. Valid values are `public_ipv4`, `public_ipv6`, and
`private_ipv4`. When unset, the builder uses the first available IP in the
default order described above.

- `public_ipv4` (string) - ID, name or IP address of a pre-allocated Hetzner
Primary IPv4 address to use for the created server.

Expand Down