-
Notifications
You must be signed in to change notification settings - Fork 458
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement Hetzner Cloud support for virtual (shared) IP
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
1 parent
95f440e
commit ba27bc3
Showing
12 changed files
with
346 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
208 changes: 208 additions & 0 deletions
208
internal/app/machined/pkg/controllers/network/operator/vip/hcloud.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.