feat: add nocloud platform support

* fetch cdrom/net nocloud config
* apply simple network configuration

Signed-off-by: Serge Logvinov <serge.logvinov@sinextra.dev>
Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
This commit is contained in:
Serge Logvinov 2021-08-28 07:56:26 +00:00 committed by Andrey Smirnov
parent 628fbf9b48
commit 353d632ae5
No known key found for this signature in database
GPG Key ID: 7B26396447AB6DFD
7 changed files with 638 additions and 2 deletions

View File

@ -540,6 +540,8 @@ local release = {
'_out/metal-pine64-arm64.img.xz',
'_out/metal-bananapi_m64-arm64.img.xz',
'_out/metal-libretech_all_h3_cc_h5-arm64.img.xz',
'_out/nocloud-amd64.raw.xz',
'_out/nocloud-arm64.raw.xz',
'_out/openstack-amd64.tar.gz',
'_out/openstack-arm64.tar.gz',
'_out/scaleway-amd64.raw.xz',

View File

@ -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-hcloud image-metal image-openstack image-scaleway image-upcloud image-vmware image-vultr ## Builds all known images (AWS, Azure, DigitalOcean, GCP, HCloud, Metal, Openstack, Scaleway, UpCloud, Vultr and VMware).
images: image-aws image-azure image-digital-ocean image-gcp image-hcloud image-metal image-nocloud image-openstack image-scaleway image-upcloud image-vmware image-vultr ## Builds all known images (AWS, Azure, DigitalOcean, GCP, HCloud, Metal, NoCloud, Openstack, Scaleway, UpCloud, Vultr 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)

View File

@ -84,7 +84,7 @@ func runImageCmd() (err error) {
if options.ConfigSource == "" {
switch p.Name() {
case "aws", "azure", "digital-ocean", "gcp", "hcloud", "scaleway", "upcloud", "vultr":
case "aws", "azure", "digital-ocean", "gcp", "hcloud", "nocloud", "scaleway", "upcloud", "vultr":
options.ConfigSource = constants.ConfigNone
case "vmware":
options.ConfigSource = constants.ConfigGuestInfo
@ -149,6 +149,19 @@ func finalize(platform runtime.Platform, img, arch string) (err error) {
log.Println("compressing image")
if err = xz(file); err != nil {
return err
}
case "nocloud":
file = filepath.Join(outputArg, fmt.Sprintf("nocloud-%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
}

View File

@ -0,0 +1,477 @@
// 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 nocloud
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"log"
"net"
"net/url"
"path/filepath"
"strings"
"github.com/AlekSi/pointer"
"github.com/talos-systems/go-blockdevice/blockdevice/filesystem"
"github.com/talos-systems/go-blockdevice/blockdevice/probe"
"github.com/talos-systems/go-procfs/procfs"
"github.com/talos-systems/go-smbios/smbios"
"golang.org/x/sys/unix"
yaml "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 (
configISOLabel = "cidata"
configNetworkConfigPath = "network-config"
configMetaDataPath = "meta-data"
configUserDataPath = "user-data"
mnt = "/mnt"
)
// NetworkConfig holds network-config info.
type NetworkConfig struct {
Version int `yaml:"version"`
Config []struct {
Mac string `yaml:"mac_address,omitempty"`
Interfaces string `yaml:"name,omitempty"`
MTU string `yaml:"mtu,omitempty"`
Subnets []struct {
Address string `yaml:"address,omitempty"`
Netmask string `yaml:"netmask,omitempty"`
Gateway string `yaml:"gateway,omitempty"`
Type string `yaml:"type"`
} `yaml:"subnets,omitempty"`
Address []string `yaml:"address,omitempty"`
Type string `yaml:"type"`
} `yaml:"config,omitempty"`
Ethernets map[string]Ethernet `yaml:"ethernets,omitempty"`
Bonds map[string]Bonds `yaml:"bonds,omitempty"`
}
// Ethernet holds network interface info.
type Ethernet struct {
Match struct {
Name []string `yaml:"name,omitempty"`
HWAddr []string `yaml:"macaddress,omitempty"`
} `yaml:"match,omitempty"`
DHCPv4 bool `yaml:"dhcp4,omitempty"`
DHCPv6 bool `yaml:"dhcp6,omitempty"`
Address []string `yaml:"addresses,omitempty"`
Gateway4 string `yaml:"gateway4,omitempty"`
Gateway6 string `yaml:"gateway6,omitempty"`
MTU int `yaml:"mtu,omitempty"`
NameServers struct {
Search []string `yaml:"search,omitempty"`
Address []string `yaml:"addresses,omitempty"`
} `yaml:"nameservers,omitempty"`
}
// Bonds holds bonding interface info.
type Bonds struct {
Interfaces []string `yaml:"interfaces,omitempty"`
Address []string `yaml:"addresses,omitempty"`
NameServers struct {
Search []string `yaml:"search,omitempty"`
Address []string `yaml:"addresses,omitempty"`
} `yaml:"nameservers,omitempty"`
Params []struct {
Mode string `yaml:"mode,omitempty"`
LACPRate string `yaml:"lacp-rate,omitempty"`
HashPolicy string `yaml:"transmit-hash-policy,omitempty"`
} `yaml:"parameters,omitempty"`
}
// MetadataConfig holds meta info.
type MetadataConfig struct {
Hostname string `yaml:"hostname,omitempty"`
InstanceID string `yaml:"instance-id,omitempty"`
}
// Nocloud is the concrete type that implements the runtime.Platform interface.
type Nocloud struct{}
// Name implements the runtime.Platform interface.
func (n *Nocloud) Name() string {
return "nocloud"
}
// ConfigurationNetwork implements the network configuration interface.
//nolint:gocyclo
func (n *Nocloud) ConfigurationNetwork(metadataNetworkConfig []byte, metadataConfig []byte, confProvider config.Provider) (config.Provider, error) {
var (
unmarshalledMetadataConfig MetadataConfig
machineConfig *v1alpha1.Config
)
if err := yaml.Unmarshal(metadataConfig, &unmarshalledMetadataConfig); err != nil {
unmarshalledMetadataConfig = MetadataConfig{}
}
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{}
}
if machineConfig.MachineConfig.MachineNetwork.NetworkHostname == "" && unmarshalledMetadataConfig.Hostname != "" {
machineConfig.MachineConfig.MachineNetwork.NetworkHostname = unmarshalledMetadataConfig.Hostname
}
if machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces == nil {
var unmarshalledNetworkConfig NetworkConfig
if err := yaml.Unmarshal(metadataNetworkConfig, &unmarshalledNetworkConfig); err != nil {
return nil, err
}
switch unmarshalledNetworkConfig.Version {
case 1:
n.applyNetworkConfigV1(unmarshalledNetworkConfig, machineConfig)
case 2:
n.applyNetworkConfigV2(unmarshalledNetworkConfig, machineConfig)
default:
return nil, fmt.Errorf("network-config metadata version=%d is not supported", unmarshalledNetworkConfig.Version)
}
}
return confProvider, nil
}
// Configuration implements the runtime.Platform interface.
//nolint:gocyclo
func (n *Nocloud) Configuration(ctx context.Context) ([]byte, error) {
s, err := smbios.New()
if err != nil {
return nil, err
}
hostname := ""
metaBaseURL := ""
networkSource := false
options := strings.Split(s.SystemInformation().SerialNumber(), ";")
for _, option := range options {
parts := strings.SplitN(option, "=", 2)
if len(parts) == 2 {
switch parts[0] {
case "ds":
if parts[1] == "nocloud-net" {
networkSource = true
}
case "s":
var u *url.URL
u, err = url.Parse(parts[1])
if err == nil && strings.HasPrefix(u.Scheme, "http") {
if strings.HasSuffix(u.Path, "/") {
metaBaseURL = parts[1]
} else {
metaBaseURL = parts[1] + "/"
}
}
case "h":
hostname = parts[1]
}
}
}
var (
metadataConfigDl []byte
metadataNetworkConfigDl []byte
machineConfigDl []byte
)
if networkSource && metaBaseURL != "" {
metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, err = n.configFromNetwork(ctx, metaBaseURL)
if err != nil {
return nil, err
}
} else {
metadataConfigDl, metadataNetworkConfigDl, machineConfigDl, err = n.configFromCD()
if err != nil {
return nil, err
}
}
if hostname != "" && metadataConfigDl == nil {
meta := &MetadataConfig{
Hostname: hostname,
}
//nolint:errcheck
metadataConfigDl, _ = yaml.Marshal(meta)
}
if bytes.HasPrefix(machineConfigDl, []byte("#cloud-config")) {
return nil, errors.ErrNoConfigSource
}
confProvider, err := configloader.NewFromBytes(machineConfigDl)
if err != nil {
return nil, err
}
confProvider, err = n.ConfigurationNetwork(metadataNetworkConfigDl, metadataConfigDl, confProvider)
if err != nil {
return nil, err
}
return confProvider.Bytes()
}
// Mode implements the runtime.Platform interface.
func (n *Nocloud) Mode() runtime.Mode {
return runtime.ModeCloud
}
// Hostname implements the runtime.Platform interface.
func (n *Nocloud) Hostname(ctx context.Context) (hostname []byte, err error) {
return nil, nil
}
// ExternalIPs implements the runtime.Platform interface.
func (n *Nocloud) ExternalIPs(ctx context.Context) (addrs []net.IP, err error) {
return addrs, nil
}
// KernelArgs implements the runtime.Platform interface.
func (n *Nocloud) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty1").Append("ttyS0"),
}
}
//nolint:gocyclo
func (n *Nocloud) applyNetworkConfigV1(networkConfig NetworkConfig, machineConfig *v1alpha1.Config) {
for _, network := range networkConfig.Config {
switch network.Type {
case "nameserver":
if machineConfig.MachineConfig.MachineNetwork.NameServers == nil {
machineConfig.MachineConfig.MachineNetwork.NameServers = network.Address
}
case "physical":
iface := v1alpha1.Device{
DeviceInterface: network.Interfaces,
DeviceDHCP: false,
}
for _, subnet := range network.Subnets {
switch subnet.Type {
case "dhcp", "dhcp4":
iface.DeviceDHCP = true
case "static":
cidr := strings.SplitN(subnet.Address, "/", 2)
if len(cidr) == 2 {
iface.DeviceAddresses = append(iface.DeviceAddresses,
subnet.Address,
)
} else {
mask, _ := net.IPMask(net.ParseIP(subnet.Netmask).To4()).Size()
iface.DeviceAddresses = append(iface.DeviceAddresses,
fmt.Sprintf("%s/%d", subnet.Address, mask),
)
}
if subnet.Gateway != "" {
iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{
RouteNetwork: "0.0.0.0/0",
RouteGateway: subnet.Gateway,
RouteMetric: 1024,
})
}
case "static6":
iface.DeviceAddresses = append(iface.DeviceAddresses,
subnet.Address,
)
if subnet.Gateway != "" {
iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{
RouteNetwork: "::/0",
RouteGateway: subnet.Gateway,
RouteMetric: 1024,
})
}
case "ipv6_dhcpv6-stateful":
iface.DeviceDHCPOptions = &v1alpha1.DHCPOptions{
DHCPIPv6: pointer.ToBool(true),
DHCPRouteMetric: 1024,
}
}
}
machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append(
machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces,
&iface,
)
}
}
}
//nolint:gocyclo
func (n *Nocloud) applyNetworkConfigV2(networkConfig NetworkConfig, machineConfig *v1alpha1.Config) {
var ns []string
for name, eth := range networkConfig.Ethernets {
if !strings.HasPrefix(name, "eth") {
continue
}
iface := v1alpha1.Device{
DeviceInterface: name,
DeviceDHCP: eth.DHCPv4,
}
if eth.DHCPv6 {
iface.DeviceDHCPOptions = &v1alpha1.DHCPOptions{
DHCPIPv6: pointer.ToBool(true),
DHCPRouteMetric: 1024,
}
}
if eth.Address != nil {
iface.DeviceAddresses = append(iface.DeviceAddresses, eth.Address...)
}
if eth.Gateway4 != "" {
iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{
RouteNetwork: "0.0.0.0/0",
RouteGateway: eth.Gateway4,
RouteMetric: 1024,
})
}
if eth.Gateway6 != "" {
iface.DeviceRoutes = append(iface.DeviceRoutes, &v1alpha1.Route{
RouteNetwork: "::/0",
RouteGateway: eth.Gateway6,
RouteMetric: 1024,
})
}
if eth.MTU != 0 {
iface.DeviceMTU = eth.MTU
}
if eth.NameServers.Address != nil {
ns = append(ns, eth.NameServers.Address...)
}
machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces = append(
machineConfig.MachineConfig.MachineNetwork.NetworkInterfaces,
&iface,
)
}
if machineConfig.MachineConfig.MachineNetwork.NameServers == nil && ns != nil {
machineConfig.MachineConfig.MachineNetwork.NameServers = ns
}
}
func (n *Nocloud) configFromNetwork(ctx context.Context, metaBaseURL string) (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) {
log.Printf("fetching meta config from: %q", metaBaseURL+configMetaDataPath)
metaConfig, err = download.Download(ctx, metaBaseURL+configMetaDataPath)
if err != nil {
metaConfig = nil
}
log.Printf("fetching network config from: %q", metaBaseURL+configNetworkConfigPath)
networkConfig, err = download.Download(ctx, metaBaseURL+configNetworkConfigPath)
if err != nil {
networkConfig = nil
}
log.Printf("fetching machine config from: %q", metaBaseURL+configUserDataPath)
machineConfig, err = download.Download(ctx, metaBaseURL+configUserDataPath,
download.WithErrorOnNotFound(errors.ErrNoConfigSource),
download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource))
if err != nil {
return nil, nil, nil, err
}
return metaConfig, networkConfig, machineConfig, nil
}
//nolint:gocyclo
func (n *Nocloud) configFromCD() (metaConfig []byte, networkConfig []byte, machineConfig []byte, err error) {
var dev *probe.ProbedBlockDevice
dev, err = probe.GetDevWithFileSystemLabel(strings.ToLower(configISOLabel))
if err != nil {
dev, err = probe.GetDevWithFileSystemLabel(strings.ToUpper(configISOLabel))
if err != nil {
return nil, nil, nil, errors.ErrNoConfigSource
}
}
//nolint:errcheck
defer dev.Close()
sb, err := filesystem.Probe(dev.Path)
if err != nil || sb == nil {
return nil, nil, nil, errors.ErrNoConfigSource
}
log.Printf("found config disk (cidata) at %s", dev.Path)
if err = unix.Mount(dev.Path, mnt, sb.Type(), unix.MS_RDONLY, ""); err != nil {
return nil, nil, nil, errors.ErrNoConfigSource
}
log.Printf("fetching meta config from: cidata/%s", configMetaDataPath)
metaConfig, err = ioutil.ReadFile(filepath.Join(mnt, configMetaDataPath))
if err != nil {
log.Printf("failed to read %s", configMetaDataPath)
metaConfig = nil
}
log.Printf("fetching network config from: cidata/%s", configNetworkConfigPath)
networkConfig, err = ioutil.ReadFile(filepath.Join(mnt, configNetworkConfigPath))
if err != nil {
log.Printf("failed to read %s", configNetworkConfigPath)
networkConfig = nil
}
log.Printf("fetching machine config from: cidata/%s", configUserDataPath)
machineConfig, err = ioutil.ReadFile(filepath.Join(mnt, configUserDataPath))
if err != nil {
log.Printf("failed to read %s", configUserDataPath)
machineConfig = nil
}
if err = unix.Unmount(mnt, 0); err != nil {
return nil, nil, nil, fmt.Errorf("failed to unmount: %w", err)
}
if machineConfig == nil {
return nil, nil, nil, errors.ErrNoConfigSource
}
return metaConfig, networkConfig, machineConfig, nil
}

View File

@ -0,0 +1,135 @@
// 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 nocloud_test
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud"
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
)
type ConfigSuite struct {
suite.Suite
}
func (suite *ConfigSuite) TestNetworkConfigV1() {
cfg := []byte(`
version: 1
config:
- type: physical
name: eth0
mac_address: 'ae:71:9e:61:d0:ad'
subnets:
- type: static
address: '192.168.1.11'
netmask: '255.255.255.0'
gateway: '192.168.1.1'
- type: static6
address: '2001:2:3:4:5:6:7:8/64'
gateway: 'fe80::1'
- type: nameserver
address:
- '192.168.1.1'
search:
- 'lan'
`)
p := &nocloud.Nocloud{}
defaultMachineConfig := &v1alpha1.Config{}
machineConfig := &v1alpha1.Config{
MachineConfig: &v1alpha1.MachineConfig{
MachineNetwork: &v1alpha1.NetworkConfig{
NetworkHostname: "talos",
NameServers: []string{"192.168.1.1"},
NetworkInterfaces: []*v1alpha1.Device{
{
DeviceInterface: "eth0",
DeviceDHCP: false,
DeviceAddresses: []string{"192.168.1.11/24", "2001:2:3:4:5:6:7:8/64"},
DeviceRoutes: []*v1alpha1.Route{
{
RouteNetwork: "0.0.0.0/0",
RouteGateway: "192.168.1.1",
RouteMetric: 1024,
},
{
RouteNetwork: "::/0",
RouteGateway: "fe80::1",
RouteMetric: 1024,
},
},
},
},
},
},
}
result, err := p.ConfigurationNetwork(cfg, []byte("hostname: talos"), defaultMachineConfig)
suite.Require().NoError(err)
suite.Assert().Equal(machineConfig, result)
}
// Network configs-v2 examples https://github.com/canonical/netplan/tree/main/examples
func (suite *ConfigSuite) TestNetworkConfigV2() {
cfg := []byte(`
version: 2
ethernets:
eth0:
dhcp4: true
addresses:
- 192.168.14.2/24
- 2001:1::1/64
gateway4: 192.168.14.1
gateway6: 2001:1::2
nameservers:
search: [foo.local, bar.local]
addresses: [8.8.8.8]
`)
p := &nocloud.Nocloud{}
defaultMachineConfig := &v1alpha1.Config{}
machineConfig := &v1alpha1.Config{
MachineConfig: &v1alpha1.MachineConfig{
MachineNetwork: &v1alpha1.NetworkConfig{
NameServers: []string{"8.8.8.8"},
NetworkInterfaces: []*v1alpha1.Device{
{
DeviceInterface: "eth0",
DeviceDHCP: true,
DeviceAddresses: []string{"192.168.14.2/24", "2001:1::1/64"},
DeviceRoutes: []*v1alpha1.Route{
{
RouteNetwork: "0.0.0.0/0",
RouteGateway: "192.168.14.1",
RouteMetric: 1024,
},
{
RouteNetwork: "::/0",
RouteGateway: "2001:1::2",
RouteMetric: 1024,
},
},
},
},
},
},
}
result, err := p.ConfigurationNetwork(cfg, []byte{}, defaultMachineConfig)
suite.Require().NoError(err)
suite.Assert().Equal(machineConfig, result)
}
func TestConfigSuite(t *testing.T) {
suite.Run(t, new(ConfigSuite))
}

View File

@ -19,6 +19,7 @@ import (
"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/nocloud"
"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"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway"
@ -71,6 +72,8 @@ func newPlatform(platform string) (p runtime.Platform, err error) {
p = &metal.Metal{}
case "openstack":
p = &openstack.Openstack{}
case "nocloud":
p = &nocloud.Nocloud{}
case "packet":
p = &packet.Packet{}
case "scaleway":

View File

@ -0,0 +1,6 @@
---
title: "Nocloud"
description: "Creating a cluster via the CLI using qemu."
---
Talos is known to work on nocloud; however, it is currently undocumented.