Skip to content

Commit

Permalink
feat: implement Hetzner Cloud support for virtual (shared) IP
Browse files Browse the repository at this point in the history
Talos supports automatic virtual IP for the control plane with pure
layer 2 connectivity. Hetzner Cloud API supports assigning Floating IPs
to the nodes, this PR combines existing virtual IP functionality with calls
to HCloud API to move the IP address on HCloud side to the leader node.

The only thing which should be supplied in the machine configuration is
the Hetzner Cloud API token, every other setting is automatically
discovered by Talos.

Talos supports two types of floating IPs:
* external Floating IP for external network
* server alias IP for local networks

The controlplane can have only one alias on the local network interface.

Signed-off-by: Serge Logvinov <serge.logvinov@sinextra.dev>
Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
  • Loading branch information
sergelogvinov authored and smira committed Sep 27, 2021
1 parent 95f440e commit ba27bc3
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/hashicorp/go-getter v1.5.8
github.com/hashicorp/go-multierror v1.1.1
github.com/hetznercloud/hcloud-go v1.32.0
github.com/imdario/mergo v0.3.12 // indirect
github.com/insomniacslk/dhcp v0.0.0-20210827173440-b95caade3eac
github.com/jsimonetti/rtnetlink v0.0.0-20210922080037-435639c8e6a8
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hetznercloud/hcloud-go v1.32.0 h1:7zyN2V7hMlhm3HZdxOarmOtvzKvkcYKjM0hcwYMQZz0=
github.com/hetznercloud/hcloud-go v1.32.0/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
Expand Down
2 changes: 2 additions & 0 deletions internal/app/machined/pkg/controllers/network/operator/vip.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ func NewVIP(logger *zap.Logger, linkName string, spec network.VIPOperatorSpec, s
switch {
case spec.EquinixMetal != network.VIPEquinixMetalSpec{}:
handler = vip.NewEquinixMetalHandler(logger, spec.IP.String(), spec.EquinixMetal)
case spec.HCloud != network.VIPHCloudSpec{}:
handler = vip.NewHCloudHandler(logger, spec.IP.String(), spec.HCloud)
default:
handler = vip.NopHandler{}
}
Expand Down
208 changes: 208 additions & 0 deletions internal/app/machined/pkg/controllers/network/operator/vip/hcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package vip

import (
"context"
"fmt"
"net"
"strconv"

"github.com/hetznercloud/hcloud-go/hcloud"
"go.uber.org/zap"
"inet.af/netaddr"

"github.com/talos-systems/talos/pkg/download"
"github.com/talos-systems/talos/pkg/resources/network"
)

// HCloudHandler implements assignment and release of Virtual IPs using API.
type HCloudHandler struct {
client *hcloud.Client

logger *zap.Logger

vip string
deviceID int
floatingID int
networkID int
}

// NewHCloudHandler creates new NewEHCloudHandler.
func NewHCloudHandler(logger *zap.Logger, vip string, spec network.VIPHCloudSpec) *HCloudHandler {
return &HCloudHandler{
client: hcloud.NewClient(hcloud.WithToken(spec.APIToken)),

logger: logger,

vip: vip,
deviceID: spec.DeviceID,
networkID: spec.NetworkID,
}
}

// Acquire implements Handler interface.
func (handler *HCloudHandler) Acquire(ctx context.Context) error {
if handler.networkID > 0 {
var action *hcloud.Action

alias := hcloud.ServerChangeAliasIPsOpts{
Network: &hcloud.Network{ID: handler.networkID},
AliasIPs: []net.IP{},
}

// trying to find the old active server
// and remove alias IP from it
serverList, err := handler.client.Server.All(ctx)
if err != nil {
return fmt.Errorf("error getting server list: %w", err)
}

oldDeviceID := findServerByAlias(serverList, handler.networkID, handler.vip)
if oldDeviceID != 0 {
action, _, err = handler.client.Server.ChangeAliasIPs(ctx,
&hcloud.Server{ID: oldDeviceID},
hcloud.ServerChangeAliasIPsOpts{
Network: &hcloud.Network{ID: handler.networkID},
AliasIPs: []net.IP{},
})
if err != nil {
return fmt.Errorf("error remove alias IPs %q on server %d: %w", handler.vip, oldDeviceID, err)
}

handler.logger.Info("cleared previous Hetzner Cloud IP alias", zap.String("vip", handler.vip),
zap.Int("device_id", oldDeviceID), zap.String("status", string(action.Status)))
}

netIP := net.ParseIP(handler.vip)
alias.AliasIPs = []net.IP{netIP}

action, _, err = handler.client.Server.ChangeAliasIPs(ctx,
&hcloud.Server{ID: handler.deviceID},
alias)
if err != nil {
return fmt.Errorf("error change alias IPs %q to server %d: %w", handler.vip, handler.deviceID, err)
}

handler.logger.Info("assigned Hetzner Cloud alias IP", zap.String("vip", handler.vip), zap.Int("device_id", handler.deviceID),
zap.Int("network_id", handler.networkID), zap.String("status", string(action.Status)))

return nil
}

floatips, err := handler.client.FloatingIP.All(ctx)
if err != nil {
return fmt.Errorf("error getting floatingIPs list: %w", err)
}

for _, floatip := range floatips {
if floatip.IP.String() == handler.vip {
action, _, err := handler.client.FloatingIP.Assign(ctx, floatip, &hcloud.Server{ID: handler.deviceID})
if err != nil {
return fmt.Errorf("error assigning %q on server %d: %w", handler.vip, handler.deviceID, err)
}

handler.logger.Info("assigned Hetzner Cloud floating IP", zap.String("vip", handler.vip), zap.Int("device_id", handler.deviceID), zap.String("status", string(action.Status)))
handler.floatingID = floatip.ID

return nil
}
}

return fmt.Errorf("error assigning %q to server %d: floating IP is not found", handler.vip, handler.deviceID)
}

// Release implements Handler interface.
func (handler *HCloudHandler) Release(ctx context.Context) error {
if handler.networkID > 0 {
alias := hcloud.ServerChangeAliasIPsOpts{
Network: &hcloud.Network{ID: handler.networkID},
AliasIPs: []net.IP{},
}

action, _, err := handler.client.Server.ChangeAliasIPs(ctx,
&hcloud.Server{ID: handler.deviceID},
alias)
if err != nil {
return fmt.Errorf("error remove alias IPs %q on server %d: %w", handler.vip, handler.deviceID, err)
}

handler.logger.Info("unassigned Hetzner Cloud alias IP", zap.String("vip", handler.vip), zap.Int("device_id", handler.deviceID),
zap.Int("network_id", handler.networkID), zap.String("status", string(action.Status)))

return nil
}

if handler.floatingID > 0 {
floatip, _, err := handler.client.FloatingIP.GetByID(ctx, handler.floatingID)
if err != nil {
return fmt.Errorf("error getting floatingIP info: %w", err)
}

if floatip.Server == nil || floatip.Server.ID != handler.deviceID {
handler.logger.Info("unassigned Hetzner Cloud floating IP", zap.String("vip", handler.vip), zap.Int("device_id", handler.deviceID))
}

handler.floatingID = 0
}

return nil
}

// HCloudMetaDataEndpoint is the local endpoint for machine info like networking.
const HCloudMetaDataEndpoint = "http://169.254.169.254/hetzner/v1/metadata/instance-id"

// GetNetworkAndDeviceIDs fills in parts of the spec based on the API token and instance metadata.
func GetNetworkAndDeviceIDs(ctx context.Context, spec *network.VIPHCloudSpec, vip netaddr.IP) error {
metadataInstanceID, err := download.Download(ctx, HCloudMetaDataEndpoint)
if err != nil {
return fmt.Errorf("error downloading instance-id: %w", err)
}

spec.DeviceID, err = strconv.Atoi(string(metadataInstanceID))
if err != nil {
return fmt.Errorf("error getting instance-id id: %w", err)
}

client := hcloud.NewClient(hcloud.WithToken(spec.APIToken))

server, _, err := client.Server.GetByID(ctx, spec.DeviceID)
if err != nil {
return fmt.Errorf("error getting server info: %w", err)
}

spec.NetworkID = 0

for _, privnet := range server.PrivateNet {
network, _, err := client.Network.GetByID(ctx, privnet.Network.ID)
if err != nil {
return fmt.Errorf("error getting network info: %w", err)
}

if network.IPRange.Contains(vip.IPAddr().IP) {
spec.NetworkID = privnet.Network.ID

break
}
}

return nil
}

func findServerByAlias(serverList []*hcloud.Server, networkID int, vip string) (deviceID int) {
for _, server := range serverList {
for _, network := range server.PrivateNet {
if network.Network.ID == networkID {
for _, alias := range network.Aliases {
if alias.String() == vip {
return server.ID
}
}
}
}
}

return 0
}
10 changes: 10 additions & 0 deletions internal/app/machined/pkg/controllers/network/operator_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ func (ctrl *OperatorConfigController) Run(ctx context.Context, r controller.Runt
} else {
specs = append(specs, spec)
}
// Hetzner Cloud VIP
case device.VIPConfig().HCloud() != nil:
spec.VIP.GratuitousARP = false
spec.VIP.HCloud.APIToken = device.VIPConfig().HCloud().APIToken()

if err = vip.GetNetworkAndDeviceIDs(ctx, &spec.VIP.HCloud, sharedIP); err != nil {
specErrors = multierror.Append(specErrors, err)
} else {
specs = append(specs, spec)
}
// Regular layer 2 VIP
default:
specs = append(specs, spec)
Expand Down
6 changes: 6 additions & 0 deletions pkg/machinery/config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,19 @@ type DHCPOptions interface {
type VIPConfig interface {
IP() string
EquinixMetal() VIPEquinixMetal
HCloud() VIPHCloud
}

// VIPEquinixMetal contains Equinix Metal API VIP settings.
type VIPEquinixMetal interface {
APIToken() string
}

// VIPHCloud contains Hetzner Cloud API VIP settings.
type VIPHCloud interface {
APIToken() string
}

// WireguardConfig contains settings for configuring Wireguard network interface.
type WireguardConfig interface {
PrivateKey() string
Expand Down
14 changes: 14 additions & 0 deletions pkg/machinery/config/types/v1alpha1/v1alpha1_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,20 @@ func (v *VIPEquinixMetalConfig) APIToken() string {
return v.EquinixMetalAPIToken
}

// HCloud implements the config.VIPConfig interface.
func (d *DeviceVIPConfig) HCloud() config.VIPHCloud {
if d.HCloudConfig == nil {
return nil
}

return d.HCloudConfig
}

// APIToken implements the config.VIPHCloud interface.
func (v *VIPHCloudConfig) APIToken() string {
return v.HCloudAPIToken
}

// WireguardConfig implements the MachineNetwork interface.
func (d *Device) WireguardConfig() config.WireguardConfig {
if d.DeviceWireguardConfig == nil {
Expand Down
8 changes: 8 additions & 0 deletions pkg/machinery/config/types/v1alpha1/v1alpha1_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,8 @@ type DeviceVIPConfig struct {
SharedIP string `yaml:"ip,omitempty"`
// description: Specifies the Equinix Metal API settings to assign VIP to the node.
EquinixMetalConfig *VIPEquinixMetalConfig `yaml:"equinixMetal,omitempty"`
// description: Specifies the Hetzner Cloud API settings to assign VIP to the node.
HCloudConfig *VIPHCloudConfig `yaml:"hcloud,omitempty"`
}

// VIPEquinixMetalConfig contains settings for Equinix Metal VIP management.
Expand All @@ -1667,6 +1669,12 @@ type VIPEquinixMetalConfig struct {
EquinixMetalAPIToken string `yaml:"apiToken"`
}

// VIPHCloudConfig contains settings for Hetzner Cloud VIP management.
type VIPHCloudConfig struct {
// description: Specifies the Hetzner Cloud API Token.
HCloudAPIToken string `yaml:"apiToken"`
}

// Bond contains the various options for configuring a bonded interface.
type Bond struct {
// description: The interfaces that make up the bond.
Expand Down
29 changes: 28 additions & 1 deletion pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go

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

0 comments on commit ba27bc3

Please sign in to comment.