Skip to content

Commit

Permalink
feat: add hetzner.com cloud support
Browse files Browse the repository at this point in the history
* cloud-init for hcloud
* set ipv6 to the 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 7, 2021
1 parent d53e9e8 commit 812d59c
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .drone.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,8 @@ local release = {
'_out/digital-ocean-arm64.tar.gz',
'_out/gcp-amd64.tar.gz',
'_out/gcp-arm64.tar.gz',
'_out/hcloud-amd64.raw.xz',
'_out/hcloud-arm64.raw.xz',
'_out/initramfs-amd64.xz',
'_out/initramfs-arm64.xz',
'_out/metal-amd64.tar.gz',
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ image-%: ## Builds the specified image. Valid options are aws, azure, digital-oc
docker run --rm -v /dev:/dev --privileged $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG) image --platform $* --arch $$arch --tar-to-stdout | tar xz -C $(ARTIFACTS) ; \
done

images: image-aws image-azure image-digital-ocean image-gcp image-metal image-openstack image-vmware ## Builds all known images (AWS, Azure, DigitalOcean, GCP, Metal, Openstack, and VMware).
images: image-aws image-azure image-digital-ocean image-gcp image-hcloud image-metal image-openstack image-vmware ## Builds all known images (AWS, Azure, DigitalOcean, GCP, HCloud, Metal, Openstack, and VMware).

sbc-%: ## Builds the specified SBC image. Valid options are rpi_4, rock64, bananapi_m64, libretech_all_h3_cc_h5, rockpi_4 and pine64 (e.g. sbc-rpi_4)
@docker pull $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG)
Expand Down
17 changes: 15 additions & 2 deletions cmd/installer/cmd/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func runImageCmd() (err error) {

if options.ConfigSource == "" {
switch p.Name() {
case "aws", "azure", "digital-ocean", "gcp":
case "aws", "azure", "digital-ocean", "gcp", "hcloud":
options.ConfigSource = constants.ConfigNone
case "vmware":
options.ConfigSource = constants.ConfigGuestInfo
Expand All @@ -109,7 +109,7 @@ func runImageCmd() (err error) {
return nil
}

//nolint:gocyclo
//nolint:gocyclo,cyclop
func finalize(platform runtime.Platform, img, arch string) (err error) {
dir := filepath.Dir(img)

Expand Down Expand Up @@ -139,6 +139,19 @@ func finalize(platform runtime.Platform, img, arch string) (err error) {
if err = tar(fmt.Sprintf("gcp-%s.tar.gz", arch), file, dir); err != nil {
return err
}
case "hcloud":
file = filepath.Join(outputArg, fmt.Sprintf("hcloud-%s.raw", arch))

err = os.Rename(img, file)
if err != nil {
return err
}

log.Println("compressing image")

if err = xz(file); err != nil {
return err
}
case "openstack":
if err = tar(fmt.Sprintf("openstack-%s.tar.gz", arch), file, dir); err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ import "errors"

// ErrNoConfigSource indicates that the platform does not have a configured source for the configuration.
var ErrNoConfigSource = errors.New("no configuration source")

// ErrNoHostname indicates that the meta server does not have a instance hostname.
var ErrNoHostname = errors.New("failed to fetch hostname from metadata service")

// ErrNoExternalIPs indicates that the meta server does not have a external addresses.
var ErrNoExternalIPs = errors.New("failed to fetch external addresses from metadata service")
200 changes: 200 additions & 0 deletions internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud/hcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// 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 hcloud

import (
"context"
"fmt"
"log"
"net"

"github.com/talos-systems/go-procfs/procfs"
"gopkg.in/yaml.v3"

"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors"
"github.com/talos-systems/talos/pkg/download"
"github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/machinery/config/configloader"
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
)

const (
// HCloudExternalIPEndpoint is the local hcloud endpoint for the external IP.
HCloudExternalIPEndpoint = "http://169.254.169.254/hetzner/v1/metadata/public-ipv4"

// HCloudNetworkEndpoint is the local hcloud endpoint for the network-config.
HCloudNetworkEndpoint = "http://169.254.169.254/hetzner/v1/metadata/network-config"

// HCloudHostnameEndpoint is the local hcloud endpoint for the hostname.
HCloudHostnameEndpoint = "http://169.254.169.254/hetzner/v1/metadata/hostname"

// HCloudUserDataEndpoint is the local hcloud endpoint for the config.
HCloudUserDataEndpoint = "http://169.254.169.254/hetzner/v1/userdata"
)

// NetworkConfig holds hcloud network-config info.
type NetworkConfig struct {
Version int `yaml:"version"`
Config []struct {
Mac string `yaml:"mac_address"`
Interfaces string `yaml:"name"`
Subnets []struct {
NameServers []string `yaml:"dns_nameservers,omitempty"`
Address string `yaml:"address,omitempty"`
Gateway string `yaml:"gateway,omitempty"`
Ipv4 bool `yaml:"ipv4,omitempty"`
Ipv6 bool `yaml:"ipv6,omitempty"`
Type string `yaml:"type"`
} `yaml:"subnets"`
Type string `yaml:"type"`
} `yaml:"config"`
}

// Hcloud is the concrete type that implements the runtime.Platform interface.
type Hcloud struct{}

// Name implements the runtime.Platform interface.
func (h *Hcloud) Name() string {
return "hcloud"
}

// ConfigurationNetwork implements the network configuration interface.
//nolint:gocyclo
func (h *Hcloud) ConfigurationNetwork(metadataNetworkConfig []byte, confProvider config.Provider) (config.Provider, error) {
var unmarshalledNetworkConfig NetworkConfig

if err := yaml.Unmarshal(metadataNetworkConfig, &unmarshalledNetworkConfig); err != nil {
return nil, err
}

if unmarshalledNetworkConfig.Version != 1 {
return nil, fmt.Errorf("network-config metadata version=%d is not supported", unmarshalledNetworkConfig.Version)
}

var machineConfig *v1alpha1.Config

machineConfig, ok := confProvider.(*v1alpha1.Config)
if !ok {
return nil, fmt.Errorf("unable to determine machine config type")
}

if machineConfig.MachineConfig == nil {
machineConfig.MachineConfig = &v1alpha1.MachineConfig{}
}

if machineConfig.MachineConfig.MachineNetwork == nil {
machineConfig.MachineConfig.MachineNetwork = &v1alpha1.NetworkConfig{}
}

for _, network := range unmarshalledNetworkConfig.Config {
iface := v1alpha1.Device{
DeviceInterface: network.Interfaces,
DeviceDHCP: false,
}

for _, subnet := range network.Subnets {
if subnet.Type == "dhcp" && subnet.Ipv4 {
iface.DeviceDHCP = true
}

if subnet.Type == "static" {
iface.DeviceAddresses = append(iface.DeviceAddresses,
subnet.Address,
)
}

if subnet.Gateway != "" && subnet.Ipv6 {
iface.DeviceRoutes = []*v1alpha1.Route{
{
RouteNetwork: "::/0",
RouteGateway: subnet.Gateway,
RouteMetric: 1024,
},
}
}
}

machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append(
machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces,
&iface,
)
}

return confProvider, nil
}

// Configuration implements the runtime.Platform interface.
func (h *Hcloud) Configuration(ctx context.Context) ([]byte, error) {
log.Printf("fetching hcloud network config from: %q", HCloudNetworkEndpoint)

metadataNetworkConfig, err := download.Download(ctx, HCloudNetworkEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to fetch network config from metadata service")
}

log.Printf("fetching machine config from: %q", HCloudUserDataEndpoint)

machineConfigDl, err := download.Download(ctx, HCloudUserDataEndpoint,
download.WithErrorOnNotFound(errors.ErrNoConfigSource),
download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource))
if err != nil {
return nil, err
}

confProvider, err := configloader.NewFromBytes(machineConfigDl)
if err != nil {
return nil, err
}

confProvider, err = h.ConfigurationNetwork(metadataNetworkConfig, confProvider)
if err != nil {
return nil, err
}

return confProvider.Bytes()
}

// Mode implements the runtime.Platform interface.
func (h *Hcloud) Mode() runtime.Mode {
return runtime.ModeCloud
}

// Hostname implements the runtime.Platform interface.
func (h *Hcloud) Hostname(ctx context.Context) (hostname []byte, err error) {
log.Printf("fetching hostname from: %q", HCloudHostnameEndpoint)

host, err := download.Download(ctx, HCloudHostnameEndpoint,
download.WithErrorOnNotFound(errors.ErrNoHostname),
download.WithErrorOnEmptyResponse(errors.ErrNoHostname))
if err != nil {
return nil, err
}

return host, nil
}

// ExternalIPs implements the runtime.Platform interface.
func (h *Hcloud) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) {
log.Printf("fetching externalIP from: %q", HCloudExternalIPEndpoint)

exIP, err := download.Download(ctx, HCloudExternalIPEndpoint,
download.WithErrorOnNotFound(errors.ErrNoExternalIPs),
download.WithErrorOnEmptyResponse(errors.ErrNoExternalIPs))
if err != nil {
return addrs, err
}

addrs = append(addrs, net.ParseIP(string(exIP)))

return addrs, err
}

// KernelArgs implements the runtime.Platform interface.
func (h *Hcloud) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty1").Append("ttyS0"),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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 hcloud_test

import (
"testing"

"github.com/stretchr/testify/suite"

"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud"
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
)

type ConfigSuite struct {
suite.Suite
}

func (suite *ConfigSuite) TestNetworkConfig() {
cfg := []byte(`
config:
- mac_address: 96:00:00:1:2:3
name: eth0
subnets:
- dns_nameservers:
- 213.133.100.100
- 213.133.99.99
- 213.133.98.98
ipv4: true
type: dhcp
- address: 2a01:4f8:1:2::1/64
gateway: fe80::1
ipv6: true
type: static
type: physical
version: 1
`)
p := &hcloud.Hcloud{}

defaultMachineConfig := &v1alpha1.Config{}

machineConfig := &v1alpha1.Config{
MachineConfig: &v1alpha1.MachineConfig{
MachineNetwork: &v1alpha1.NetworkConfig{
NetworkInterfaces: []*v1alpha1.Device{
{
DeviceInterface: "eth0",
DeviceDHCP: true,
DeviceAddresses: []string{"2a01:4f8:1:2::1/64"},
DeviceRoutes: []*v1alpha1.Route{
{
RouteNetwork: "::/0",
RouteGateway: "fe80::1",
RouteMetric: 1024,
},
},
},
},
},
},
}

result, err := p.ConfigurationNetwork(cfg, defaultMachineConfig)

suite.Require().NoError(err)
suite.Assert().Equal(machineConfig, result)
}

func TestConfigSuite(t *testing.T) {
suite.Run(t, new(ConfigSuite))
}

// http://169.254.169.254/hetzner/v1/metadata/network-config
// config:
// - mac_address: 96:00:00:72:a3:19
// name: eth0
// subnets:
// - dns_nameservers:
// - 213.133.100.100
// - 213.133.99.99
// - 213.133.98.98
// ipv4: true
// type: dhcp
// - address: 2a01:4f8:1:2::1/64
// gateway: fe80::1
// ipv6: true
// type: static
// type: physical
// version: 1
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import (
)

const (
// OpenstackExternalIPEndpoint is the local EC2 endpoint for the external IP.
// OpenstackExternalIPEndpoint is the local Openstack endpoint for the external IP.
OpenstackExternalIPEndpoint = "http://169.254.169.254/latest/meta-data/public-ipv4"

// OpenstackHostnameEndpoint is the local EC2 endpoint for the hostname.
// OpenstackHostnameEndpoint is the local Openstack endpoint for the hostname.
OpenstackHostnameEndpoint = "http://169.254.169.254/latest/meta-data/hostname"

// OpenstackUserDataEndpoint is the local EC2 endpoint for the config.
// OpenstackUserDataEndpoint is the local Openstack endpoint for the config.
OpenstackUserDataEndpoint = "http://169.254.169.254/latest/user-data"
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/container"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/digitalocean"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/gcp"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/packet"
Expand Down Expand Up @@ -48,6 +49,7 @@ func NewPlatform(platform string) (p runtime.Platform, err error) {
return newPlatform(platform)
}

//nolint:gocyclo
func newPlatform(platform string) (p runtime.Platform, err error) {
switch platform {
case "aws":
Expand All @@ -60,6 +62,8 @@ func newPlatform(platform string) (p runtime.Platform, err error) {
p = &digitalocean.DigitalOcean{}
case "gcp":
p = &gcp.GCP{}
case "hcloud":
p = &hcloud.Hcloud{}
case "metal":
p = &metal.Metal{}
case "openstack":
Expand Down

0 comments on commit 812d59c

Please sign in to comment.