Skip to content

Commit 0239528

Browse files
authored
fix(builder): refactor network_interfaces handling (#23)
- drop public IPv4 requirement during build - new `default` IP address flag to select used interface/IP during build - new `wait_boot` flag adds ability to wait N time for server to boot up and start all services - add `none` communicator support - support for IPv6 interfaces
1 parent 1bcfc50 commit 0239528

File tree

18 files changed

+655
-65
lines changed

18 files changed

+655
-65
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/)
66
## [Unreleased]
77

88
### Added
9-
- add new template name param
9+
- add new template name param
10+
- new `default` IP address flag to select used interface/IP during build
11+
- new `wait_boot` flag adds ability to wait N time for server to boot up and start all services
12+
- add `none` communicator support
13+
- support for IPv6 interfaces
1014

1115
### Changed
1216
- update README file
1317
- update acceptance test to embed HCL2 configs
18+
- drop public IPv4 interface requirement
1419

1520
### Fixed
1621
- fix network interface config

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ test:
2020

2121
test_integration: build
2222
cp $(BINARY) builder/upcloud/
23-
PACKER_ACC=1 go test -count 1 -v ./... -timeout=120m
23+
PACKER_ACC=1 go test -count 1 -v $(TESTARGS) ./... -timeout=120m
2424

2525
lint:
2626
go vet .
@@ -49,6 +49,7 @@ generate: fmt install-packer-sdc
4949
$(PACKER_SDC_RENDER_DOCS)
5050

5151
fmt:
52+
packer fmt builder/upcloud/test-fixtures/hcl2
5253
packer fmt example/
5354
packer fmt -recursive docs-partials/
5455

builder/upcloud/builder.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
6868
Config: &b.config,
6969
GeneratedData: generatedData,
7070
},
71-
&communicator.StepConnect{
72-
Config: &b.config.Comm,
73-
Host: sshHostCallback,
74-
SSHConfig: b.config.Comm.SSHConfigFunc(),
75-
},
71+
b.communicatorStep(),
7672
&commonsteps.StepProvision{},
7773
&commonsteps.StepCleanupTempKeys{
7874
Comm: &b.config.Comm,
@@ -111,3 +107,21 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
111107

112108
return artifact, nil
113109
}
110+
111+
// CommunicatorStep returns step based on communicator type
112+
// We currently support only SSH communicator but 'none' type
113+
// can also be used for e.g. testing purposes
114+
func (b *Builder) communicatorStep() multistep.Step {
115+
switch b.config.Comm.Type {
116+
case "none":
117+
return &communicator.StepConnect{
118+
Config: &b.config.Comm,
119+
}
120+
default:
121+
return &communicator.StepConnect{
122+
Config: &b.config.Comm,
123+
Host: sshHostCallback,
124+
SSHConfig: b.config.Comm.SSHConfigFunc(),
125+
}
126+
}
127+
}

builder/upcloud/builder_acc_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ var testBuilderStorageUuidHcl string
8787
//go:embed test-fixtures/hcl2/storage-name.pkr.hcl
8888
var testBuilderStorageNameHcl string
8989

90+
//go:embed test-fixtures/hcl2/network_interfaces.pkr.hcl
91+
var testBuilderNetworkInterfacesHcl string
92+
9093
func TestBuilderAcc_default_hcl(t *testing.T) {
9194
testAccPreCheck(t)
9295
testCase := &acctest.PluginTestCase{
@@ -120,6 +123,28 @@ func TestBuilderAcc_storageName_hcl(t *testing.T) {
120123
acctest.TestPlugin(t, testCase)
121124
}
122125

126+
func TestBuilderAcc_network_interfaces(t *testing.T) {
127+
testAccPreCheck(t)
128+
testCase := &acctest.PluginTestCase{
129+
Name: t.Name(),
130+
Template: testBuilderNetworkInterfacesHcl,
131+
Check: func(buildCommand *exec.Cmd, logfile string) error {
132+
re := regexp.MustCompile(`upcloud.network_interfaces: Selecting default ip '10.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}' as Server IP`)
133+
log, err := readLog(logfile)
134+
if err != nil {
135+
return err
136+
}
137+
fmt.Println(log)
138+
if !re.MatchString(log) {
139+
return fmt.Errorf("Unable find default utility network IP from the log %s", logfile)
140+
}
141+
return nil
142+
},
143+
Teardown: teardown(t.Name()),
144+
}
145+
acctest.TestPlugin(t, testCase)
146+
}
147+
123148
func testAccPreCheck(t *testing.T) {
124149
if v := os.Getenv("UPCLOUD_API_USER"); v == "" {
125150
t.Skip("UPCLOUD_API_USER must be set for acceptance tests")

builder/upcloud/config.go

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@ import (
1616
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
1717
)
1818

19+
type InterfaceType string
20+
1921
const (
20-
DefaultTemplatePrefix = "custom-image"
21-
DefaultSSHUsername = "root"
22-
DefaultStorageSize = 25
23-
DefaultTimeout = 5 * time.Minute
22+
DefaultTemplatePrefix = "custom-image"
23+
DefaultSSHUsername = "root"
24+
DefaultCommunicator = "ssh"
25+
DefaultStorageSize = 25
26+
DefaultTimeout = 5 * time.Minute
27+
InterfaceTypePublic InterfaceType = upcloud.IPAddressAccessPublic
28+
InterfaceTypeUtility InterfaceType = upcloud.IPAddressAccessUtility
29+
InterfaceTypePrivate InterfaceType = upcloud.IPAddressAccessPrivate
2430
)
2531

2632
var (
@@ -38,13 +44,24 @@ var (
3844

3945
// for config type convertion
4046
type NetworkInterface struct {
47+
// List of IP Addresses
4148
IPAddresses []IPAddress `mapstructure:"ip_addresses"`
42-
Type string `mapstructure:"type"`
43-
Network string `mapstructure:"network,omitempty"`
49+
50+
// Network type (e.g. public, utility, private)
51+
Type InterfaceType `mapstructure:"type"`
52+
53+
// Network UUID when connecting private network
54+
Network string `mapstructure:"network,omitempty"`
4455
}
4556

4657
type IPAddress struct {
47-
Family string `mapstructure:"family"`
58+
// Default IP address. When set to `true` SSH communicator will connect to this IP after boot.
59+
Default bool `mapstructure:"default"`
60+
61+
// IP address family (IPv4 or IPv6)
62+
Family string `mapstructure:"family"`
63+
64+
// IP address. Note that at the moment using floating IPs is not supported.
4865
Address string `mapstructure:"address,omitempty"`
4966
}
5067

@@ -89,6 +106,9 @@ type Config struct {
89106
// The amount of time to wait for resource state changes. Defaults to `5m`.
90107
Timeout time.Duration `mapstructure:"state_timeout_duration"`
91108

109+
// The amount of time to wait after booting the server. Defaults to '0s'
110+
BootWait time.Duration `mapstructure:"boot_wait"`
111+
92112
// The array of extra zones (locations) where created templates should be cloned.
93113
// Note that default `state_timeout_duration` is not enough for cloning, better to increase a value depending on storage size.
94114
CloneZones []string `mapstructure:"clone_zones"`
@@ -105,6 +125,18 @@ type Config struct {
105125
ctx interpolate.Context
106126
}
107127

128+
// DefaultIPaddress returns default IP address and its type (public,private,utility)
129+
func (c *Config) DefaultIPaddress() (*IPAddress, InterfaceType) {
130+
for _, iface := range c.NetworkInterfaces {
131+
for _, addr := range iface.IPAddresses {
132+
if addr.Default {
133+
return &addr, iface.Type
134+
}
135+
}
136+
}
137+
return nil, ""
138+
}
139+
108140
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
109141
err := config.Decode(c, &config.DecodeOpts{
110142
Interpolate: true,
@@ -130,7 +162,11 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
130162
c.Timeout = DefaultTimeout
131163
}
132164

133-
if c.Comm.SSHUsername == "" {
165+
if c.Comm.Type == "" {
166+
c.Comm.Type = DefaultCommunicator
167+
}
168+
169+
if c.Comm.Type == "ssh" && c.Comm.SSHUsername == "" {
134170
c.Comm.SSHUsername = DefaultSSHUsername
135171
}
136172

builder/upcloud/config.hcl2spec.go

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builder/upcloud/step_create_server.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package upcloud
33
import (
44
"context"
55
"fmt"
6+
"time"
67

78
"github.com/UpCloudLtd/packer-plugin-upcloud/internal/driver"
89
"github.com/hashicorp/packer-plugin-sdk/multistep"
@@ -52,23 +53,36 @@ func (s *StepCreateServer) Run(_ context.Context, state multistep.StateBag) mult
5253
return stepHaltWithError(state, err)
5354
}
5455

55-
serverUuid := response.UUID
56-
serverTitle := response.Title
57-
serverIp, err := getServerIp(response)
58-
if err != nil {
59-
return stepHaltWithError(state, err)
56+
ui.Say(fmt.Sprintf("Server %q created and in 'started' state", response.Title))
57+
58+
addr, infType := s.Config.DefaultIPaddress()
59+
if addr != nil {
60+
if addr.Address == "" {
61+
addr, err = findIPAddressByType(response.IPAddresses, infType)
62+
if err != nil {
63+
return stepHaltWithError(state, err)
64+
}
65+
}
66+
ui.Say(fmt.Sprintf("Selecting default ip '%s' as Server IP", addr.Address))
67+
} else {
68+
addr, err = findIPAddressByType(response.IPAddresses, InterfaceTypePublic)
69+
if err != nil {
70+
return stepHaltWithError(state, err)
71+
}
72+
ui.Say(fmt.Sprintf("Auto-selecting ip '%s' as Server IP", addr.Address))
6073
}
6174

62-
ui.Say(fmt.Sprintf("Server %q created and in 'started' state", serverTitle))
63-
64-
state.Put("server_uuid", serverUuid)
65-
state.Put("server_title", serverTitle)
66-
state.Put("server_ip", serverIp)
67-
68-
s.GeneratedData.Put("ServerUUID", serverUuid)
69-
s.GeneratedData.Put("ServerTitle", serverTitle)
70-
s.GeneratedData.Put("ServerSize", serverIp)
75+
state.Put("server_ip_address", addr)
76+
state.Put("server_uuid", response.UUID)
77+
state.Put("server_title", response.Title)
7178

79+
s.GeneratedData.Put("ServerUUID", response.UUID)
80+
s.GeneratedData.Put("ServerTitle", response.Title)
81+
s.GeneratedData.Put("ServerSize", response.Plan)
82+
if s.Config.BootWait > 0 {
83+
ui.Say(fmt.Sprintf("Waitig boot: %s", s.Config.BootWait.String()))
84+
time.Sleep(s.Config.BootWait)
85+
}
7286
return multistep.ActionContinue
7387
}
7488

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
source "upcloud" "network_interfaces" {
2+
storage_name = "Debian GNU/Linux 11 (Bullseye)"
3+
storage_size = 10
4+
zone = "nl-ams1"
5+
6+
network_interfaces {
7+
ip_addresses {
8+
family = "IPv4"
9+
}
10+
type = "public"
11+
}
12+
13+
network_interfaces {
14+
ip_addresses {
15+
default = true
16+
family = "IPv4"
17+
}
18+
type = "utility"
19+
}
20+
21+
communicator = "none"
22+
boot_wait = "1m"
23+
}
24+
25+
build {
26+
sources = ["source.upcloud.network_interfaces"]
27+
}

builder/upcloud/utils.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package upcloud
22

33
import (
4+
"errors"
45
"fmt"
56
"time"
67

@@ -18,23 +19,43 @@ func stepHaltWithError(state multistep.StateBag, err error) multistep.StepAction
1819
return multistep.ActionHalt
1920
}
2021

21-
// parse public ip from server details
22-
func getServerIp(details *upcloud.ServerDetails) (string, error) {
23-
for _, ipAddress := range details.IPAddresses {
24-
if ipAddress.Access == upcloud.IPAddressAccessPublic && ipAddress.Family == upcloud.IPAddressFamilyIPv4 {
25-
return ipAddress.Address, nil
22+
// Find IP address by type from list of IP addresses
23+
func findIPAddressByType(addrs upcloud.IPAddressSlice, infType InterfaceType) (*IPAddress, error) {
24+
var ipv6 *IPAddress
25+
for _, ipAddress := range addrs {
26+
if ipAddress.Access == string(infType) {
27+
switch ipAddress.Family {
28+
case upcloud.IPAddressFamilyIPv4:
29+
// prefer IPv4 over IPv6 - return first matching IPv4 interface if found
30+
return &IPAddress{Address: ipAddress.Address, Family: ipAddress.Family}, nil
31+
case upcloud.IPAddressFamilyIPv6:
32+
// not returning IPv6 because there might be IPv4 address comming up in the slice
33+
ipv6 = &IPAddress{Address: ipAddress.Address, Family: ipAddress.Family}
34+
}
2635
}
2736
}
28-
return "", fmt.Errorf("Unable to find the public IPv4 address of the server")
37+
// return IPv6 if found
38+
if ipv6 != nil {
39+
return ipv6, nil
40+
}
41+
return nil, fmt.Errorf("Unable to find '%s' IP address", infType)
2942
}
3043

3144
func getNowString() string {
3245
return time.Now().Format("20060102-150405")
3346
}
3447

35-
// sshHostCallback retrieves the public IPv4 address of the server
48+
// sshHostCallback returns server's IP addresss.
49+
// Note that IPv6 address needs to be enclosed in square brackets
3650
func sshHostCallback(state multistep.StateBag) (string, error) {
37-
return state.Get("server_ip").(string), nil
51+
addr, ok := state.Get("server_ip_address").(*IPAddress)
52+
if !ok || addr == nil {
53+
return "", errors.New("unable to get server_ip_address from state")
54+
}
55+
if addr.Family == upcloud.IPAddressFamilyIPv6 {
56+
return fmt.Sprintf("[%s]", addr.Address), nil
57+
}
58+
return addr.Address, nil
3859
}
3960

4061
func convertNetworkTypes(rawNetworking []NetworkInterface) []request.CreateServerInterface {
@@ -46,7 +67,7 @@ func convertNetworkTypes(rawNetworking []NetworkInterface) []request.CreateServe
4667
}
4768
networking = append(networking, request.CreateServerInterface{
4869
IPAddresses: ips,
49-
Type: iface.Type,
70+
Type: string(iface.Type),
5071
Network: iface.Network,
5172
})
5273
}

0 commit comments

Comments
 (0)