feat: add OpenNebula platform support

Initial support without documentation.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
Signed-off-by: shurkys <no@mail.com>
This commit is contained in:
shurkys 2024-02-03 13:15:34 +00:00 committed by Andrey Smirnov
parent 914f887788
commit 989ca3ade1
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
12 changed files with 465 additions and 2 deletions

View File

@ -932,6 +932,8 @@ local release = {
'_out/metal-nanopi_r4s-arm64.raw.xz',
'_out/nocloud-amd64.raw.xz',
'_out/nocloud-arm64.raw.xz',
'_out/opennebula-amd64.raw.xz',
'_out/opennebula-arm64.raw.xz',
'_out/openstack-amd64.raw.xz',
'_out/openstack-arm64.raw.xz',
'_out/oracle-amd64.qcow2.xz',

View File

@ -317,7 +317,7 @@ image-%: ## Builds the specified image. Valid options are aws, azure, digital-oc
images-essential: image-aws image-gcp image-metal secureboot-installer ## Builds only essential images used in the CI (AWS, GCP, and Metal).
images: image-aws image-azure image-digital-ocean image-exoscale image-gcp image-hcloud image-iso image-metal image-nocloud image-openstack image-oracle image-scaleway image-upcloud image-vmware image-vultr ## Builds all known images (AWS, Azure, DigitalOcean, Exoscale, GCP, HCloud, Metal, NoCloud, Openstack, Oracle, Scaleway, UpCloud, Vultr and VMware).
images: image-aws image-azure image-digital-ocean image-exoscale image-gcp image-hcloud image-iso image-metal image-nocloud image-opennebula image-openstack image-oracle image-scaleway image-upcloud image-vmware image-vultr ## Builds all known images (AWS, Azure, DigitalOcean, Exoscale, GCP, HCloud, Metal, NoCloud, OpenNebula, Openstack, Oracle, Scaleway, UpCloud, Vultr and VMware).
sbc-%: ## Builds the specified SBC image. Valid options are rpi_generic, rock64, bananapi_m64, libretech_all_h3_cc_h5, rockpi_4, rockpi_4c, pine64, jetson_nano and nanopi_r4s (e.g. sbc-rpi_generic)
@docker pull $(REGISTRY_AND_USERNAME)/imager:$(IMAGE_TAG)

View File

@ -55,6 +55,12 @@ machine:
title = "Kubernetes API Server Service Account Key"
description = """\
Talos Linux starting from this release uses RSA key for Kubernetes API Server Service Account instead of ECDSA key to provide better compatibility with external OpenID Connect implementations.
"""
[notes.opennebula]
title = "OpenNebula"
description = """\
Talos Linux now supports OpenNebula platform.
"""
[make_deps]

View File

@ -0,0 +1,69 @@
// 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 opennebula provides the OpenNebula platform implementation.
package opennebula
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/siderolabs/go-blockdevice/blockdevice/filesystem"
"github.com/siderolabs/go-blockdevice/blockdevice/probe"
"golang.org/x/sys/unix"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors"
)
const (
configISOLabel = "context"
oneContextPath = "context.sh"
mnt = "/mnt"
)
func (o *OpenNebula) contextFromCD() (oneContext []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, fmt.Errorf("failed to find %s iso: %w", configISOLabel, err)
}
}
//nolint:errcheck
defer dev.Close()
sb, err := filesystem.Probe(dev.Path)
if err != nil || sb == nil {
return nil, errors.ErrNoConfigSource
}
log.Printf("found config disk (context) at %s", dev.Path)
if err = unix.Mount(dev.Path, mnt, sb.Type(), unix.MS_RDONLY, ""); err != nil {
return nil, fmt.Errorf("failed to mount iso: %w", err)
}
log.Printf("fetching context from: %s/", oneContextPath)
oneContext, err = os.ReadFile(filepath.Join(mnt, oneContextPath))
if err != nil {
return nil, fmt.Errorf("read config: %s", err.Error())
}
if err = unix.Unmount(mnt, 0); err != nil {
return nil, fmt.Errorf("failed to unmount: %w", err)
}
if oneContext == nil {
return nil, errors.ErrNoConfigSource
}
return oneContext, nil
}

View File

@ -0,0 +1,252 @@
// 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 opennebula
import (
"bytes"
"context"
"encoding/base64"
stderrors "errors"
"fmt"
"net/netip"
"strconv"
"strings"
"github.com/cosi-project/runtime/pkg/state"
"github.com/hashicorp/go-envparse"
"github.com/siderolabs/go-procfs/procfs"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/errors"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/internal/address"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
"github.com/siderolabs/talos/pkg/machinery/resources/network"
runtimeres "github.com/siderolabs/talos/pkg/machinery/resources/runtime"
)
// OpenNebula is the concrete type that implements the runtime.Platform interface.
type OpenNebula struct{}
// Name implements the runtime.Platform interface.
func (o *OpenNebula) Name() string {
return "opennebula"
}
// ParseMetadata converts opennebula metadata to platform network config.
//
//nolint:gocyclo
func (o *OpenNebula) ParseMetadata(st state.State, oneContextPlain []byte) (*runtime.PlatformNetworkConfig, error) {
// Initialize the PlatformNetworkConfig
networkConfig := &runtime.PlatformNetworkConfig{}
oneContext, err := envparse.Parse(bytes.NewReader(oneContextPlain))
if err != nil {
return nil, fmt.Errorf("failed to parse context file %q: %w", oneContextPlain, err)
}
// Create HostnameSpecSpec entry
hostnameValue := oneContext["HOSTNAME"]
if hostnameValue == "" {
hostnameValue = oneContext["SET_HOSTNAME"]
if hostnameValue == "" {
hostnameValue = oneContext["NAME"]
}
}
if oneContext["NETWORK"] == "YES" {
// Iterate through parsed environment variables
for key := range oneContext {
// Dereference the pointer here
if strings.HasPrefix(key, "ETH") && strings.HasSuffix(key, "_MAC") {
ifaceName := strings.TrimSuffix(key, "_MAC")
ifaceNameLower := strings.ToLower(ifaceName)
if oneContext[ifaceName+"_METHOD"] == "dhcp" {
// Create DHCP4 OperatorSpec entry
networkConfig.Operators = append(networkConfig.Operators,
network.OperatorSpecSpec{
Operator: network.OperatorDHCP4,
LinkName: ifaceNameLower,
RequireUp: true,
DHCP4: network.DHCP4OperatorSpec{
RouteMetric: 1024,
SkipHostnameRequest: true,
},
ConfigLayer: network.ConfigPlatform,
},
)
} else {
// Parse IP address and create AddressSpecSpec entry
ipPrefix, err := address.IPPrefixFrom(oneContext[ifaceName+"_IP"], oneContext[ifaceName+"_MASK"])
if err != nil {
return nil, fmt.Errorf("failed to parse IP address: %w", err)
}
networkConfig.Addresses = append(networkConfig.Addresses,
network.AddressSpecSpec{
Address: ipPrefix,
LinkName: ifaceNameLower,
Family: nethelpers.FamilyInet4,
Scope: nethelpers.ScopeGlobal,
Flags: nethelpers.AddressFlags(nethelpers.AddressPermanent),
AnnounceWithARP: false,
ConfigLayer: network.ConfigPlatform,
},
)
var mtu uint32
if oneContext[ifaceName+"_MTU"] == "" {
mtu = 0
} else {
var mtu64 uint64
mtu64, err = strconv.ParseUint(oneContext[ifaceName+"_MTU"], 10, 32)
// check if any error happened
if err != nil {
return nil, fmt.Errorf("failed to parse MTU: %w", err)
}
mtu = uint32(mtu64)
}
// Create LinkSpecSpec entry
networkConfig.Links = append(networkConfig.Links,
network.LinkSpecSpec{
Name: ifaceNameLower,
Logical: false,
Up: true,
MTU: mtu,
Kind: "",
Type: nethelpers.LinkEther,
ParentName: "",
ConfigLayer: network.ConfigPlatform,
},
)
// Parse gateway address and create RouteSpecSpec entry
gateway, err := netip.ParseAddr(oneContext[ifaceName+"_GATEWAY"])
if err != nil {
return nil, fmt.Errorf("failed to parse gateway ip: %w", err)
}
route := network.RouteSpecSpec{
ConfigLayer: network.ConfigPlatform,
Gateway: gateway,
OutLinkName: ifaceNameLower,
Table: nethelpers.TableMain,
Protocol: nethelpers.ProtocolStatic,
Type: nethelpers.TypeUnicast,
Family: nethelpers.FamilyInet4,
Priority: network.DefaultRouteMetric,
}
route.Normalize()
networkConfig.Routes = append(networkConfig.Routes, route)
// Parse DNS servers
dnsServers := strings.Fields(oneContext[ifaceName+"_DNS"])
var dnsIPs []netip.Addr
for _, dnsServer := range dnsServers {
ip, err := netip.ParseAddr(dnsServer)
if err != nil {
return nil, fmt.Errorf("failed to parse DNS server IP: %w", err)
}
dnsIPs = append(dnsIPs, ip)
}
// Create ResolverSpecSpec entry with multiple DNS servers
networkConfig.Resolvers = append(networkConfig.Resolvers,
network.ResolverSpecSpec{
DNSServers: dnsIPs,
ConfigLayer: network.ConfigPlatform,
},
)
}
}
}
}
// Create HostnameSpecSpec entry
networkConfig.Hostnames = append(networkConfig.Hostnames,
network.HostnameSpecSpec{
Hostname: hostnameValue,
Domainname: oneContext["DNS_HOSTNAME"],
ConfigLayer: network.ConfigPlatform,
},
)
// Create Metadata entry
networkConfig.Metadata = &runtimeres.PlatformMetadataSpec{
Platform: o.Name(),
Hostname: hostnameValue,
InstanceID: oneContext["VMID"],
}
return networkConfig, nil
}
// Configuration implements the runtime.Platform interface.
func (o *OpenNebula) Configuration(ctx context.Context, r state.State) (machineConfig []byte, err error) {
oneContextPlain, err := o.contextFromCD()
if err != nil {
return nil, err
}
oneContext, err := envparse.Parse(bytes.NewReader(oneContextPlain))
if err != nil {
return nil, fmt.Errorf("failed to parse environment file %q: %w", oneContextPlain, err)
}
userData, ok := oneContext["USER_DATA"]
if !ok {
return nil, errors.ErrNoConfigSource
}
machineConfig, err = base64.StdEncoding.DecodeString(userData)
if err != nil {
return nil, fmt.Errorf("failed to decode USER_DATA: %v", err)
}
return machineConfig, nil
}
// Mode implements the runtime.Platform interface.
func (o *OpenNebula) Mode() runtime.Mode {
return runtime.ModeCloud
}
// KernelArgs implements the runtime.Platform interface.
func (o *OpenNebula) KernelArgs() procfs.Parameters {
return []*procfs.Parameter{
procfs.NewParameter("console").Append("tty1").Append("ttyS0"),
procfs.NewParameter(constants.KernelParamNetIfnames).Append("0"),
}
}
// NetworkConfiguration implements the runtime.Platform interface.
func (o *OpenNebula) NetworkConfiguration(ctx context.Context, st state.State, ch chan<- *runtime.PlatformNetworkConfig) error {
oneContext, err := o.contextFromCD()
if stderrors.Is(err, errors.ErrNoConfigSource) {
err = nil
}
if err != nil {
return err
}
networkConfig, err := o.ParseMetadata(st, oneContext)
if err != nil {
return err
}
select {
case ch <- networkConfig:
case <-ctx.Done():
return ctx.Err()
}
return nil
}

View File

@ -0,0 +1,40 @@
// 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/.
// go test -v ./internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula
package opennebula_test
import (
_ "embed"
"fmt"
"testing"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula"
)
//go:embed testdata/metadata.yaml
var oneContextPlain []byte
//go:embed testdata/expected.yaml
var expectedNetworkConfig string
func TestParseMetadata(t *testing.T) {
o := &opennebula.OpenNebula{}
st := state.WrapCore(namespaced.NewState(inmem.Build))
networkConfig, err := o.ParseMetadata(st, oneContextPlain)
require.NoError(t, err)
marshaled, err := yaml.Marshal(networkConfig)
require.NoError(t, err)
fmt.Print(marshaled)
assert.Equal(t, expectedNetworkConfig, string(marshaled))
}

View File

@ -0,0 +1,45 @@
addresses:
- address: 192.168.1.92/24
linkName: eth0
family: inet4
scope: global
flags: permanent
layer: platform
links:
- name: eth0
logical: false
up: true
mtu: 0
kind: ""
type: ether
layer: platform
routes:
- family: inet4
dst: ""
src: ""
gateway: 192.168.1.1
outLinkName: eth0
table: main
priority: 1024
scope: global
type: unicast
flags: ""
protocol: static
layer: platform
hostnames:
- hostname: code-server
domainname: ""
layer: platform
resolvers:
- dnsServers:
- 192.168.1.1
- 8.8.8.8
- 1.1.1.1
layer: platform
timeServers: []
operators: []
externalIPs: []
metadata:
platform: opennebula
hostname: code-server
instanceId: "14"

View File

@ -0,0 +1,28 @@
# Context variables generated by OpenNebula
DISK_ID = "1"
ETH0_DNS = "192.168.1.1 8.8.8.8 1.1.1.1"
ETH0_EXTERNAL = ""
ETH0_GATEWAY = "192.168.1.1"
ETH0_IP = "192.168.1.92"
ETH0_IP6 = ""
ETH0_IP6_GATEWAY = ""
ETH0_IP6_METHOD = ""
ETH0_IP6_METRIC = ""
ETH0_IP6_PREFIX_LENGTH = ""
ETH0_IP6_ULA = ""
ETH0_MAC = "02:00:c0:a8:01:5c"
ETH0_MASK = "255.255.255.0"
ETH0_METHOD = ""
ETH0_METRIC = ""
ETH0_MTU = ""
ETH0_NETWORK = "192.168.1.0"
ETH0_SEARCH_DOMAIN = ""
ETH0_VLAN_ID = "3"
ETH0_VROUTER_IP = ""
ETH0_VROUTER_IP6 = ""
ETH0_VROUTER_MANAGEMENT = ""
NETWORK = "YES"
SSH_PUBLIC_KEY = ""
TARGET = "hda"
VMID = "14"
NAME = "code-server"

View File

@ -25,6 +25,7 @@ import (
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/hcloud"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/metal"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/nocloud"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/opennebula"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/openstack"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/oracle"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform/scaleway"
@ -103,6 +104,8 @@ func newPlatform(platform string) (p runtime.Platform, err error) {
p = &hcloud.Hcloud{}
case constants.PlatformMetal:
p = &metal.Metal{}
case "opennebula":
p = &opennebula.OpenNebula{}
case "openstack":
p = &openstack.Openstack{}
case "oracle":

View File

@ -167,6 +167,18 @@ var Default = map[string]Profile{
},
},
},
"opennebula": {
Platform: "opennebula",
SecureBoot: pointer.To(false),
Output: Output{
Kind: OutKindImage,
OutFormat: OutFormatXZ,
ImageOptions: &ImageOptions{
DiskSize: MinRAWDiskSize,
DiskFormat: DiskFormatRaw,
},
},
},
"openstack": {
Platform: "openstack",
SecureBoot: pointer.To(false),

View File

@ -12,7 +12,7 @@ description: "Table of supported Talos Linux versions and respective platforms."
| Kubernetes | 1.30, 1.29, 1.28, 1.27, 1.26, 1.25 | 1.29, 1.28, 1.27, 1.26, 1.25, 1.24 |
| Architecture | amd64, arm64 | amd64, arm64 |
| **Platforms** | | |
| - cloud | AWS, GCP, Azure, Digital Ocean, Exoscale, Hetzner, OpenStack, Oracle Cloud, Scaleway, Vultr, Upcloud | AWS, GCP, Azure, Digital Ocean, Exoscale, Hetzner, OpenStack, Oracle Cloud, Scaleway, Vultr, Upcloud |
| - cloud | AWS, GCP, Azure, Digital Ocean, Exoscale, Hetzner, OpenNebula, OpenStack, Oracle Cloud, Scaleway, Vultr, Upcloud | AWS, GCP, Azure, Digital Ocean, Exoscale, Hetzner, OpenStack, Oracle Cloud, Scaleway, Vultr, Upcloud |
| - bare metal | x86: BIOS, UEFI, SecureBoot; arm64: UEFI, SecureBoot; boot: ISO, PXE, disk image | x86: BIOS, UEFI; arm64: UEFI; boot: ISO, PXE, disk image |
| - virtualized | VMware, Hyper-V, KVM, Proxmox, Xen | VMware, Hyper-V, KVM, Proxmox, Xen |
| - SBCs | Banana Pi M64, Jetson Nano, Libre Computer Board ALL-H3-CC, Nano Pi R4S, Pine64, Pine64 Rock64, Radxa ROCK Pi 4c, Raspberry Pi 4B, Raspberry Pi Compute Module 4 | Banana Pi M64, Jetson Nano, Libre Computer Board ALL-H3-CC, Nano Pi R4S, Pine64, Pine64 Rock64, Radxa ROCK Pi 4c, Raspberry Pi 4B, Raspberry Pi Compute Module 4 |
@ -46,6 +46,7 @@ description: "Table of supported Talos Linux versions and respective platforms."
* Exoscale
* Hetzner
* nocloud
* OpenNebula
* Oracle Cloud
* Scaleway
* Vultr

View File

@ -0,0 +1,5 @@
---
title: "OpenNebula"
---
Talos is known to work on [OpenNebula](https://opennebula.io/).