Skip to content

Commit

Permalink
feat: add OpenNebula platform support
Browse files Browse the repository at this point in the history
Initial support without documentation.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
Signed-off-by: shurkys <no@mail.com>
  • Loading branch information
shurkys authored and smira committed Feb 5, 2024
1 parent 914f887 commit 989ca3a
Show file tree
Hide file tree
Showing 12 changed files with 465 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .drone.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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))
}
Loading

0 comments on commit 989ca3a

Please sign in to comment.